Use updated cache daemon (#2475)

* feat: 🎸 Handle truncated cache daemon response
This commit is contained in:
Zhihe Li committed 2024-09-24 14:30:52 -04:00
1 parent ace65eab3a
commit 97258f7525
15 files changed
+468 -478

No files matched your search

@@ -7,6 +7,7 @@ import sinon from 'sinon';
import { typeOf } from '@ember/utils';
import { resourceNames } from 'api/handlers/cache-daemon-handler';
import { singularize } from 'ember-inflector';
import { underscore } from '@ember/string';
/**
* This test helper can be used to help setup your sinon stubs in your tests.
@@ -69,16 +70,18 @@ export default function setupStubs(hooks) {
.withArgs('searchCacheDaemon')
.onCall(i)
.returns({
[resourceName]: models.map((model) => {
// Use internal serializer to serialize the model correctly
// according to our mirage serializers
const modelData =
this.server.serializerOrRegistry.serialize(model);
[underscore(resourceNames[singularize(resourceName)])]: models.map(
(model) => {
// Use internal serializer to serialize the model correctly
// according to our mirage serializers
const modelData =
this.server.serializerOrRegistry.serialize(model);
// Serialize the data properly to standard JSON as that is what
// we're expecting from the cache daemon response
return JSON.parse(JSON.stringify(modelData));
}),
// Serialize the data properly to standard JSON as that is what
// we're expecting from the cache daemon response
return JSON.parse(JSON.stringify(modelData));
},
),
});
});
};
@@ -8,6 +8,7 @@ import { inject as service } from '@ember/service';
import { getOwner, setOwner } from '@ember/application';
import { generateMQLExpression } from '../utils/mql-query';
import { paginateResults } from '../utils/paginate-results';
import { underscore } from '@ember/string';
/**
* Not all types are yet supported by the cache daemon so we'll
@@ -94,12 +95,14 @@ export default class CacheDaemonHandler {
const sessionData = this.session.data?.authenticated;
const auth_token_id = sessionData?.id;
const token = sessionData?.token;
const resourceName = resourceNames[type];
remainingQuery = {
...remainingQuery,
query: searchQuery,
auth_token_id,
token,
resource: resourceNames[type],
resource: resourceName,
};
let cacheDaemonResults = {};
@@ -130,7 +133,7 @@ export default class CacheDaemonHandler {
}
} else {
__electronLog?.error(
'Failed to search cache daemon; falling back to search controller',
`Failed to search cache daemon for ${resourceName}. Error:`,
e.message,
);
@@ -138,10 +141,7 @@ export default class CacheDaemonHandler {
}
}
// Currently returns with a singular top level field with resource name
// e.g. { targets: [...] } or { sessions: [..] }
// So this just unwraps to the array, or undefined
const [results] = Object.values(cacheDaemonResults);
const results = cacheDaemonResults[underscore(resourceName)];
const payload = { items: paginateResults(results, page, pageSize) };
const schema = store.modelFor(type);
@@ -167,7 +167,15 @@ export default class CacheDaemonHandler {
// This isn't conventional but is better than returning an ArrayProxy
// or EmberArray since the ember store query method asserts it has to be an array
// so we can't just return an object.
records.meta = { totalItems: results?.length ?? 0 };
// Also include whether the results are incomplete or is still refreshing
// as part of the response
records.meta = {
totalItems: results?.length ?? 0,
isLoadIncomplete: cacheDaemonResults.incomplete ?? false,
// Sometimes the refresh status returns an error status if it takes too long but it's still refreshing
isRefreshing: cacheDaemonResults.refresh_status !== 'not-refreshing',
};
return records;
}
default:
+3 -6
View File
@@ -7,7 +7,6 @@
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 1rem;
gap: 0.5rem;
.filter-tag {
@@ -27,18 +26,16 @@
margin-top: 3rem;
}
.hds-segmented-group {
> * {
margin-bottom: 1rem;
}
.hds-segmented-group {
.hds-dropdown-toggle-button__text {
white-space: nowrap;
}
}
.hds-table {
margin-bottom: 1rem;
}
.search-filtering-toolbar {
display: flex;
@@ -24,3 +24,6 @@ loading:
title: Loading all items...
description: All of your {resource} are still loading. Please wait while we finish loading everything on screen.
generic-description: All of your resources are still loading. Please wait while we finish loading everything on screen.
incomplete-loading:
limit: We've reached the result limit. Please narrow down your search or filters if the result you're looking for is not shown.
refreshing: We're still currently loading the results. Please wait a moment before refreshing again.
@@ -54,19 +54,6 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll
);
}
/**
* Returns targets that are associated will all sessions the user has access to
* @returns {[TargetModel]}
*/
get availableTargets() {
const uniqueSessionTargetIds = new Set(
this.model.allSessions.map((session) => session.target_id),
);
return this.model.allTargets.filter((target) =>
uniqueSessionTargetIds.has(target.id),
);
}
/**
* Returns all status types for sessions
* @returns {[object]}
@@ -98,7 +85,7 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll
get filters() {
return {
allFilters: {
targets: this.availableTargets,
targets: this.model.associatedTargets,
status: this.sessionStatusOptions,
scopes: this.availableScopes,
},
@@ -49,15 +49,6 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
// =methods
get showFilters() {
return (
this.model.allTargets.length ||
this.types.length ||
this.search ||
this.availableSessions.length
);
}
/**
* Returns true if model is empty but we have a search term or filters
* @returns {boolean}
@@ -85,13 +76,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
* @returns {[ScopeModel]}
*/
get availableScopes() {
const uniqueTargetScopeIds = new Set(
this.model.allTargets.map((target) => target.scope.id),
);
return this.model.projects.filter((project) =>
uniqueTargetScopeIds.has(project.id),
);
// TODO: Make use of implicit scopes or when scopes are added to cache daemon
return this.model.projects;
}
get availableSessionOptions() {
@@ -8,9 +8,10 @@ import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import {
STATUS_SESSION_ACTIVE,
STATUS_SESSION_PENDING,
STATUS_SESSION_CANCELING,
STATUS_SESSION_PENDING,
} from 'api/models/session';
import chunk from 'lodash/chunk';
export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
// =services
@@ -44,7 +45,6 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
};
allSessions;
allTargets;
// =methods
@@ -65,7 +65,7 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
/**
* Loads queried sessions, the total number of sessions, all sessions,
* all targets and all projects for filtering options for the current user.
* @return {Promise<{sessions: [SessionModel], projects: [ScopeModel], allSessions: [SessionModel], allTargets: [TargetModel], totalItems: number}>}
* @return {Promise<{sessions: [SessionModel], projects: [ScopeModel], allSessions: [SessionModel], associatedTargets: [TargetModel], totalItems: number}>}
*/
async model({ targets, status, scopes, page, pageSize }) {
const projects = this.modelFor('scopes.scope.projects');
@@ -97,6 +97,7 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
page,
pageSize,
force_refresh: true,
max_result_set_size: -1,
};
if (orgScope.isOrg && scopes.length === 0) {
queryOptions.filter = orgFilter;
@@ -104,36 +105,62 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
const sessions = await this.store.query('session', queryOptions);
const totalItems = sessions.meta?.totalItems;
// Query all sessions and all targets for defining filtering values if entering route for the first time
// Query all sessions and associated targets for defining filtering values if entering route for the first time
if (!this.allSessions?.length) {
await this.getAllSessions(orgScope, scopes, orgFilter);
}
if (!this.allTargets?.length) {
await this.getAllTargets(orgScope, orgFilter);
}
const associatedTargets = await this.getAssociatedTargets(
orgScope,
orgFilter,
);
return {
sessions,
projects,
allSessions: this.allSessions,
allTargets: this.allTargets,
associatedTargets,
totalItems,
isCacheDaemonRunning: await this.ipc.invoke('isCacheDaemonRunning'),
};
}
async getAllTargets(orgScope, orgFilter) {
const allTargetsQuery = {
scope_id: orgScope.id,
recursive: true,
force_refresh: true,
};
if (orgScope.isOrg) {
allTargetsQuery.filter = orgFilter;
async getAssociatedTargets(orgScope) {
if (!this.allSessions) {
return [];
}
this.allTargets = await this.store.query('target', allTargetsQuery, {
pushToStore: false,
});
const uniqueTargetIds = [
...new Set(this.allSessions.map((session) => session.target_id)),
];
// Chunk target IDs in case there's a lot of sessions as cache daemon syntax can't take too many
const chunkedTargetIds = chunk(uniqueTargetIds, 50);
const associatedTargetsPromises = chunkedTargetIds.map((targetIds) =>
this.store.query(
'target',
{
scope_id: orgScope.id,
recursive: true,
force_refresh: true,
query: {
filters: {
id: {
logicalOperator: 'or',
values: targetIds.map((targetId) => ({
equals: targetId,
})),
},
},
},
},
{
pushToStore: false,
},
),
);
const targetsArray = await Promise.all(associatedTargetsPromises);
return targetsArray.flat();
}
async getAllSessions(orgScope, scopes, orgFilter) {
@@ -146,6 +173,7 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
},
},
force_refresh: true,
max_result_set_size: -1,
};
if (orgScope.isOrg && scopes.length === 0) {
allSessionsQuery.filter = orgFilter;
@@ -182,8 +210,8 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
const orgScope = this.modelFor('scopes.scope');
const orgFilter = `"/item/scope/parent_scope_id" == "${orgScope.id}"`;
await this.getAllTargets(orgScope, orgFilter);
await this.getAllSessions(orgScope, [], orgFilter);
await this.getAssociatedTargets(orgScope);
// Prime the store by searching for orgs only in case there are new org scopes;
// otherwise we won't be able to correctly peek the org scopes for their display name in the UI.
@@ -49,8 +49,6 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
},
};
allTargets;
// =methods
/**
@@ -79,8 +77,12 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
* projects for filtering options.
*
* @returns {Promise<{totalItems: number, targets: [TargetModel], projects: [ScopeModel], allTargets: [TargetModel] }> }
* @return {Promise<{totalItems: number, targets: [TargetModel], projects: [ScopeModel],
* isCacheDaemonRunning: boolean, isLoadIncomplete: boolean, isRefreshing: boolean}>}
*/
async model({ search, scopes, availableSessions, types, page, pageSize }) {
const isCacheRunningPromise = this.ipc.invoke('isCacheDaemonRunning');
const orgScope = this.modelFor('scopes.scope');
const projects = this.modelFor('scopes.scope.projects');
@@ -96,15 +98,7 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
filters.type.push({ equals: type });
});
const aliasPromise = this.store.query('alias', {
scope_id: 'global',
force_refresh: true,
});
const allTargetsPromise = !this.allTargets?.length
? this.makeAllTargetsQuery(orgScope, orgFilter)
: Promise.resolve();
const sessions = await this.makeSessionQuery(orgScope, scopes, orgFilter);
const sessions = await this.getSessions(orgScope, scopes, orgFilter);
this.addActiveSessionFilters(filters, availableSessions, sessions);
const query = {
@@ -119,20 +113,28 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
query.filter = orgFilter;
}
let targets = await this.store.query('target', query);
const totalItems = targets.meta?.totalItems;
const { totalItems, isLoadIncomplete, isRefreshing } = targets.meta;
// Filter out targets to which users do not have the connect ability
targets = targets.filter((target) =>
this.can.can('connect target', target),
);
const allTargets = await allTargetsPromise;
if (!this.allTargets?.length) {
// Filter out targets to which users do not have the connect ability
this.allTargets = allTargets.filter((target) =>
target.authorized_actions.includes('authorize-session'),
);
}
const aliasPromise = this.store.query('alias', {
scope_id: 'global',
force_refresh: true,
query: {
filters: {
destination_id: {
logicalOperator: 'or',
values: targets.map((target) => ({
equals: target.id,
})),
},
},
},
});
// Load the aliases for the targets on the current page
try {
await aliasPromise;
} catch (e) {
@@ -144,13 +146,14 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
return {
targets,
projects,
allTargets: this.allTargets,
totalItems,
isCacheDaemonRunning: await this.ipc.invoke('isCacheDaemonRunning'),
isLoadIncomplete,
isRefreshing,
isCacheDaemonRunning: await isCacheRunningPromise,
};
}
async makeSessionQuery(orgScope, scopes, orgFilter) {
async getSessions(orgScope, scopes, orgFilter) {
// Retrieve all sessions so that the session and activeSessions getters
// in the target model always retrieve the most up-to-date sessions.
const sessionQuery = {
@@ -166,6 +169,7 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
},
},
force_refresh: true,
max_result_set_size: -1,
};
if (orgScope.isOrg && scopes.length === 0) {
sessionQuery.filter = orgFilter;
@@ -173,27 +177,6 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
return this.store.query('session', sessionQuery);
}
/**
* Get all the targets but only load them once when entering the route. Ideally we would lazy load it when needed
* but this can be revisited in the future.
* @param orgScope
* @param orgFilter
* @returns {Promise<void>}
*/
async makeAllTargetsQuery(orgScope, orgFilter) {
// Query all targets for defining filtering values if entering route for first time
const query = {
scope_id: orgScope.id,
recursive: true,
force_refresh: true,
};
if (orgScope.isOrg) {
query.filter = orgFilter;
}
const options = { pushToStore: false };
return this.store.query('target', query, options);
}
/**
* Add the filters for active sessions to the filter object.
* @param filters
@@ -255,14 +238,6 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
@action
async refreshAll() {
const orgScope = this.modelFor('scopes.scope');
const orgFilter = `"/item/scope/parent_scope_id" == "${orgScope.id}"`;
const allTargets = await this.makeAllTargetsQuery(orgScope, orgFilter);
// Filter out targets to which users do not have the connect ability
this.allTargets = allTargets.filter((target) =>
target.authorized_actions.includes('authorize-session'),
);
// Prime the store by searching for only orgs in case there are new org scopes;
// otherwise we won't be able to correctly peek the org scopes for their display name in the UI.
await this.store.query('scope', {});
@@ -3,119 +3,118 @@
SPDX-License-Identifier: BUSL-1.1
}}
{{#if (or @model.sessions @model.allSessions)}}
{{#if @model.isCacheDaemonRunning}}
<div class='search-filtering-toolbar'>
<Hds::SegmentedGroup as |S|>
<S.Generic>
<Dropdown
name='target'
@toggleText={{t 'resources.target.title'}}
@itemOptions={{this.availableTargets}}
@checkedItems={{this.targets}}
@applyFilter={{fn this.applyFilter 'targets'}}
@isSearchable={{true}}
@listPosition='bottom-left'
/>
</S.Generic>
<S.Generic>
<Dropdown
name='status'
@toggleText={{t 'form.status.label'}}
@itemOptions={{this.sessionStatusOptions}}
@checkedItems={{this.status}}
@applyFilter={{fn this.applyFilter 'status'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
name='scope'
@toggleText={{t 'resources.scope.title'}}
@itemOptions={{this.availableScopes}}
@checkedItems={{this.scopes}}
@applyFilter={{fn this.applyFilter 'scopes'}}
@isSearchable={{true}}
as |FD selectItem itemOptions|
>
{{#let (group-by itemOptions 'scopeModel') as |orgs|}}
{{#each orgs as |org|}}
<FD.Generic>
<FlightIcon @name='org' />
{{org.key.displayName}}
</FD.Generic>
{{#each org.items as |project|}}
<FD.Checkbox
@value={{project.id}}
checked={{includes project.id this.scopes}}
{{on 'change' selectItem}}
>
{{project.displayName}}
</FD.Checkbox>
{{/each}}
{{/each}}
{{/let}}
</Dropdown>
</S.Generic>
</Hds::SegmentedGroup>
<span>
<ToolbarRefresher @onClick={{route-action 'refreshAll'}} />
</span>
</div>
<FilterTags @filters={{this.filters}} />
{{/if}}
{{#if @model.sessions}}
<Hds::Table
@model={{this.sortedSessions}}
@columns={{array
(hash label=(t 'form.started.label'))
(hash label=(t 'form.session_id.label'))
(hash label=(t 'resources.target.title'))
(hash label=(t 'form.proxy_url.label'))
(hash label=(t 'form.status.label'))
(hash
label=(t 'titles.actions')
isVisuallyHidden=true
align='right'
width='60px'
)
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<div class='link-copy-button-container'>
{{#if B.data.isAvailable}}
<Hds::Link::Inline
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-session-detail-link={{B.data.id}}
{{#if @model.isCacheDaemonRunning}}
<div class='search-filtering-toolbar'>
<Hds::SegmentedGroup as |S|>
<S.Generic>
<Dropdown
name='target'
@toggleText={{t 'resources.target.title'}}
@itemOptions={{this.model.associatedTargets}}
@checkedItems={{this.targets}}
@applyFilter={{fn this.applyFilter 'targets'}}
@isSearchable={{true}}
@listPosition='bottom-left'
/>
</S.Generic>
<S.Generic>
<Dropdown
name='status'
@toggleText={{t 'form.status.label'}}
@itemOptions={{this.sessionStatusOptions}}
@checkedItems={{this.status}}
@applyFilter={{fn this.applyFilter 'status'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
name='scope'
@toggleText={{t 'resources.scope.title'}}
@itemOptions={{this.availableScopes}}
@checkedItems={{this.scopes}}
@applyFilter={{fn this.applyFilter 'scopes'}}
@isSearchable={{true}}
as |FD selectItem itemOptions|
>
{{#let (group-by itemOptions 'scopeModel') as |orgs|}}
{{#each orgs as |org|}}
<FD.Generic>
<FlightIcon @name='org' />
{{org.key.displayName}}
</FD.Generic>
{{#each org.items as |project|}}
<FD.Checkbox
@value={{project.id}}
checked={{includes project.id this.scopes}}
{{on 'change' selectItem}}
>
{{B.data.id}}
</Hds::Link::Inline>
{{else}}
<Hds::Text::Body data-test-session-id-copy={{B.data.id}}>
{{B.data.id}}
</Hds::Text::Body>
{{/if}}
<Hds::Copy::Button
@text={{B.data.id}}
@isIconOnly={{true}}
@textToCopy={{B.data.id}}
data-test-session-id-copy={{B.data.id}}
/>
</div>
</B.Td>
<B.Td>
{{B.data.target.displayName}}
</B.Td>
{{!
{{project.displayName}}
</FD.Checkbox>
{{/each}}
{{/each}}
{{/let}}
</Dropdown>
</S.Generic>
</Hds::SegmentedGroup>
<span>
<ToolbarRefresher @onClick={{route-action 'refreshAll'}} />
</span>
</div>
<FilterTags @filters={{this.filters}} />
{{/if}}
{{#if @model.sessions}}
<Hds::Table
@model={{this.sortedSessions}}
@columns={{array
(hash label=(t 'form.started.label'))
(hash label=(t 'form.session_id.label'))
(hash label=(t 'resources.target.title'))
(hash label=(t 'form.proxy_url.label'))
(hash label=(t 'form.status.label'))
(hash
label=(t 'titles.actions')
isVisuallyHidden=true
align='right'
width='60px'
)
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<div class='link-copy-button-container'>
{{#if B.data.isAvailable}}
<Hds::Link::Inline
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-session-detail-link={{B.data.id}}
>
{{B.data.id}}
</Hds::Link::Inline>
{{else}}
<Hds::Text::Body data-test-session-id-copy={{B.data.id}}>
{{B.data.id}}
</Hds::Text::Body>
{{/if}}
<Hds::Copy::Button
@text={{B.data.id}}
@isIconOnly={{true}}
@textToCopy={{B.data.id}}
data-test-session-id-copy={{B.data.id}}
/>
</div>
</B.Td>
<B.Td>
{{B.data.target.displayName}}
</B.Td>
{{!
Two if's here because:
1) Local proxy info is only relevant if the session is
active or pending (aka isAvailable). Once the session
@@ -126,50 +125,49 @@
from other devices (or even another tab), in which case we
won't have the local proxy info available here.
}}
<B.Td>
{{#if B.data.isAvailable}}
{{#if B.data.proxy}}
<Hds::Copy::Snippet
@textToCopy={{B.data.proxy}}
@color='secondary'
/>
{{/if}}
{{/if}}
</B.Td>
<B.Td>
<SessionStatus @model={{B.data}} />
</B.Td>
<B.Td @align='right' width='60px'>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
@text={{t 'actions.cancel'}}
@icon='x'
@isIconOnly={{true}}
<B.Td>
{{#if B.data.isAvailable}}
{{#if B.data.proxy}}
<Hds::Copy::Snippet
@textToCopy={{B.data.proxy}}
@color='secondary'
{{on 'click' (route-action 'cancelSession' B.data)}}
data-test-session-cancel-button={{B.data.id}}
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
{{/if}}
</B.Td>
<B.Td>
<SessionStatus @model={{B.data}} />
</B.Td>
<B.Td @align='right' width='60px'>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
@text={{t 'actions.cancel'}}
@icon='x'
@isIconOnly={{true}}
@color='secondary'
{{on 'click' (route-action 'cancelSession' B.data)}}
data-test-session-cancel-button={{B.data.id}}
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
{{else if @model.allSessions}}
<Hds::ApplicationState data-test-no-session-results as |A|>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t
'descriptions.no-filter-results'
resource=(t 'resources.session.title_plural')
}}
/>
{{else}}
<Hds::ApplicationState data-test-no-session-results as |A|>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t
'descriptions.no-filter-results'
resource=(t 'resources.session.title_plural')
}}
/>
</Hds::ApplicationState>
{{/if}}
</Hds::ApplicationState>
{{else}}
<Hds::ApplicationState data-test-no-sessions as |A|>
<A.Header @title={{t 'resources.session.messages.none-friendly.title'}} />
@@ -3,191 +3,199 @@
SPDX-License-Identifier: BUSL-1.1
}}
{{#if this.showFilters}}
{{#if @model.isCacheDaemonRunning}}
<div class='search-filtering-toolbar'>
<Hds::SegmentedGroup as |S|>
<S.TextInput
@value={{this.search}}
@type='search'
placeholder={{t 'actions.search'}}
aria-label={{t 'actions.search'}}
{{on 'input' this.handleSearchInput}}
/>
<S.Generic>
<Dropdown
name='active-sessions'
@toggleText={{t 'resources.target.titles.active-sessions'}}
@itemOptions={{this.availableSessionOptions}}
@checkedItems={{this.availableSessions}}
@applyFilter={{fn this.applyFilter 'availableSessions'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
name='type'
@toggleText={{t 'form.type.label'}}
@itemOptions={{this.targetTypeOptions}}
@checkedItems={{this.types}}
@applyFilter={{fn this.applyFilter 'types'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
name='scope'
@toggleText={{t 'resources.scope.title'}}
@itemOptions={{this.availableScopes}}
@checkedItems={{this.scopes}}
@applyFilter={{fn this.applyFilter 'scopes'}}
@isSearchable={{true}}
as |FD selectItem itemOptions|
>
{{#let (group-by itemOptions 'scopeModel') as |orgs|}}
{{#each orgs as |org|}}
<FD.Generic>
<FlightIcon @name='org' />
{{org.key.displayName}}
</FD.Generic>
{{#each org.items as |project|}}
<FD.Checkbox
@value={{project.id}}
data-test-checkbox={{project.name}}
checked={{includes project.id this.scopes}}
{{on 'change' selectItem}}
>
{{project.displayName}}
</FD.Checkbox>
{{/each}}
{{/each}}
{{/let}}
</Dropdown>
</S.Generic>
</Hds::SegmentedGroup>
<span>
<ToolbarRefresher @onClick={{route-action 'refreshAll'}} />
</span>
</div>
<FilterTags @filters={{this.filters}} />
{{/if}}
{{#if @model.targets}}
<Hds::Table
@model={{@model.targets}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'resources.alias.title_plural'))
(hash label=(t 'resources.target.titles.active-sessions'))
(hash label=(t 'form.type.label'))
(hash label=(t 'resources.project.title'))
(hash label=(t 'titles.actions') isVisuallyHidden=true align='right')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<span class='hds-font-weight-semibold'>
{{#if (can 'read model' B.data)}}
<LinkTo
data-test-visit-target={{B.data.id}}
@route='scopes.scope.projects.targets.target'
@model={{B.data.id}}
@query={{hash isConnecting=false}}
>
{{B.data.displayName}}
</LinkTo>
{{else}}
{{B.data.displayName}}
{{/if}}
{{#if B.data.description}}
<p>{{B.data.description}}</p>
{{/if}}
</span>
</B.Td>
<B.Td data-test-target-aliases={{B.data.id}}>
{{truncate-list 'actions.more' B.data.associatedAliases}}
</B.Td>
<B.Td>
{{#if B.data.isActive}}
<Hds::Button
data-test-targets-sessions-flyout-button={{B.data.id}}
@text={{t 'actions.yes'}}
@color='tertiary'
@icon='info'
{{on 'click' (fn this.selectTarget B.data)}}
/>
{{/if}}
</B.Td>
<B.Td>
{{#if B.data.type}}
<Hds::Badge
@text={{t (concat 'resources.target.types.' B.data.type)}}
/>
{{/if}}
</B.Td>
<B.Td>
<div>
<Hds::Copy::Snippet
@textToCopy={{B.data.project.id}}
@color='secondary'
data-test-target-project-id={{B.data.project.id}}
/>
</div>
{{#if B.data.project.name}}
<Hds::Badge @color='neutral' @text={{B.data.project.name}} />
{{/if}}
</B.Td>
<B.Td @align='right'>
<div {{style display='inline-block'}}>
{{#if (can 'connect target' B.data)}}
{{#if (can 'read target' B.data)}}
<Hds::Button
data-test-targets-connect-button={{B.data.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
@route='scopes.scope.projects.targets.target'
@model={{B.data.id}}
@query={{hash isConnecting=true}}
/>
{{else}}
<Hds::Button
data-test-targets-connect-button={{B.data.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
{{on 'click' (fn this.quickConnect B.data)}}
/>
{{/if}}
{{/if}}
</div>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
{{/if}}
{{#if this.noResults}}
<Hds::ApplicationState data-test-no-target-results as |A|>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t
(if
this.search
'descriptions.no-search-results'
'descriptions.no-filter-results'
)
query=this.search
resource=(t 'resources.target.title_plural')
}}
{{#if @model.isCacheDaemonRunning}}
<div class='search-filtering-toolbar'>
<Hds::SegmentedGroup as |S|>
<S.TextInput
@value={{this.search}}
@type='search'
placeholder={{t 'actions.search'}}
aria-label={{t 'actions.search'}}
{{on 'input' this.handleSearchInput}}
/>
</Hds::ApplicationState>
<S.Generic>
<Dropdown
name='active-sessions'
@toggleText={{t 'resources.target.titles.active-sessions'}}
@itemOptions={{this.availableSessionOptions}}
@checkedItems={{this.availableSessions}}
@applyFilter={{fn this.applyFilter 'availableSessions'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
name='type'
@toggleText={{t 'form.type.label'}}
@itemOptions={{this.targetTypeOptions}}
@checkedItems={{this.types}}
@applyFilter={{fn this.applyFilter 'types'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
name='scope'
@toggleText={{t 'resources.scope.title'}}
@itemOptions={{this.availableScopes}}
@checkedItems={{this.scopes}}
@applyFilter={{fn this.applyFilter 'scopes'}}
@isSearchable={{true}}
as |FD selectItem itemOptions|
>
{{#let (group-by itemOptions 'scopeModel') as |orgs|}}
{{#each orgs as |org|}}
<FD.Generic>
<FlightIcon @name='org' />
{{org.key.displayName}}
</FD.Generic>
{{#each org.items as |project|}}
<FD.Checkbox
@value={{project.id}}
data-test-checkbox={{project.name}}
checked={{includes project.id this.scopes}}
{{on 'change' selectItem}}
>
{{project.displayName}}
</FD.Checkbox>
{{/each}}
{{/each}}
{{/let}}
</Dropdown>
</S.Generic>
</Hds::SegmentedGroup>
<span>
<ToolbarRefresher @onClick={{route-action 'refreshAll'}} />
</span>
</div>
<FilterTags @filters={{this.filters}} />
{{#if @model.isLoadIncomplete}}
<Hds::Alert @color='warning' @type='compact' as |A|>
<A.Description>{{t 'states.incomplete-loading.limit'}}</A.Description>
</Hds::Alert>
{{/if}}
{{#if @model.isRefreshing}}
<Hds::Alert @color='warning' @type='compact' as |A|>
<A.Description>{{t
'states.incomplete-loading.refreshing'
}}</A.Description>
</Hds::Alert>
{{/if}}
{{/if}}
{{#if this.noTargets}}
{{#if @model.targets}}
<Hds::Table
@model={{@model.targets}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'resources.alias.title_plural'))
(hash label=(t 'resources.target.titles.active-sessions'))
(hash label=(t 'form.type.label'))
(hash label=(t 'resources.project.title'))
(hash label=(t 'titles.actions') isVisuallyHidden=true align='right')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<span class='hds-font-weight-semibold'>
{{#if (can 'read model' B.data)}}
<LinkTo
data-test-visit-target={{B.data.id}}
@route='scopes.scope.projects.targets.target'
@model={{B.data.id}}
@query={{hash isConnecting=false}}
>
{{B.data.displayName}}
</LinkTo>
{{else}}
{{B.data.displayName}}
{{/if}}
{{#if B.data.description}}
<p>{{B.data.description}}</p>
{{/if}}
</span>
</B.Td>
<B.Td data-test-target-aliases={{B.data.id}}>
{{truncate-list 'actions.more' B.data.associatedAliases}}
</B.Td>
<B.Td>
{{#if B.data.isActive}}
<Hds::Button
data-test-targets-sessions-flyout-button={{B.data.id}}
@text={{t 'actions.yes'}}
@color='tertiary'
@icon='info'
{{on 'click' (fn this.selectTarget B.data)}}
/>
{{/if}}
</B.Td>
<B.Td>
{{#if B.data.type}}
<Hds::Badge
@text={{t (concat 'resources.target.types.' B.data.type)}}
/>
{{/if}}
</B.Td>
<B.Td>
<div>
<Hds::Copy::Snippet
@textToCopy={{B.data.project.id}}
@color='secondary'
data-test-target-project-id={{B.data.project.id}}
/>
</div>
{{#if B.data.project.name}}
<Hds::Badge @color='neutral' @text={{B.data.project.name}} />
{{/if}}
</B.Td>
<B.Td @align='right'>
<div {{style display='inline-block'}}>
{{#if (can 'connect target' B.data)}}
{{#if (can 'read target' B.data)}}
<Hds::Button
data-test-targets-connect-button={{B.data.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
@route='scopes.scope.projects.targets.target'
@model={{B.data.id}}
@query={{hash isConnecting=true}}
/>
{{else}}
<Hds::Button
data-test-targets-connect-button={{B.data.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
{{on 'click' (fn this.quickConnect B.data)}}
/>
{{/if}}
{{/if}}
</div>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
{{/if}}
{{#if this.noResults}}
<Hds::ApplicationState data-test-no-target-results as |A|>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t
(if
this.search
'descriptions.no-search-results'
'descriptions.no-filter-results'
)
query=this.search
resource=(t 'resources.target.title_plural')
}}
/>
</Hds::ApplicationState>
{{else if this.noTargets}}
<Hds::ApplicationState data-test-no-targets as |A|>
<A.Header @title={{t 'resources.target.messages.none.title'}} />
<A.Body @text={{t 'resources.target.messages.none.description'}} />
@@ -60,7 +60,7 @@ const unixSocketRequest = (options, reqBody) =>
if (statusCode < 200 || statusCode >= 400) {
reject({
statusCode,
message: parsedResponse?.api_error ?? parsedResponse?.error,
message: parsedResponse?.message ?? parsedResponse?.error,
});
}
@@ -186,7 +186,7 @@ module('Acceptance | projects | sessions | index', function (hooks) {
test('visiting empty sessions', async function (assert) {
this.server.db.sessions.remove();
this.stubCacheDaemonSearch('sessions', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'sessions');
await visit(urls.projects);
await click(`[href="${urls.sessions}"]`);
@@ -371,14 +371,14 @@ module('Acceptance | projects | sessions | index', function (hooks) {
'sessions',
'sessions',
'targets',
'aliases',
'targets',
'sessions',
'targets',
'aliases',
{
resource: 'sessions',
func: () => [instances.session2],
},
'targets',
);
await visit(urls.globalSessions);
@@ -161,7 +161,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
setDefaultClusterUrl(this);
this.ipcStub.withArgs('isCacheDaemonRunning').returns(true);
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
@@ -207,7 +207,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('visiting targets list view with no targets', async function (assert) {
this.server.db.targets.remove();
this.server.db.sessions.remove();
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
await visit(urls.projects);
@@ -219,7 +219,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('user cannot navigate to a target without proper authorization', async function (assert) {
instances.target.authorized_actions =
instances.target.authorized_actions.filter((item) => item !== 'read');
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
await visit(urls.projects);
@@ -248,7 +248,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
instances.target.authorized_actions.filter(
(item) => item !== 'authorize-session',
);
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
await visit(urls.projects);
@@ -281,7 +281,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('user can connect without target read permissions', async function (assert) {
instances.target.authorized_actions =
instances.target.authorized_actions.filter((item) => item !== 'read');
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
this.ipcStub.withArgs('cliExists').returns(true);
this.ipcStub.withArgs('connect').returns({
session_id: instances.session.id,
@@ -307,7 +307,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
instances.target.authorized_actions.filter((item) => item !== 'read');
this.ipcStub.withArgs('cliExists').returns(true);
this.ipcStub.withArgs('connect').rejects();
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.projects);
@@ -348,8 +348,6 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('user can cancel a session from inside target sessions flyout', async function (assert) {
this.stubCacheDaemonSearch(
'aliases',
'targets',
'sessions',
'targets',
'aliases',
@@ -358,6 +356,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
func: () => [],
},
'targets',
'aliases',
);
await visit(urls.projects);
@@ -387,7 +386,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('user cannot cancel a session from inside target sessions flyout without permissions', async function (assert) {
instances.session.authorized_actions =
instances.session.authorized_actions.filter((item) => item !== 'cancel');
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
await visit(urls.projects);
await click(`[href="${urls.targets}"]`);
@@ -425,7 +424,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('user can navigate to session details from sessions table in flyout without permissions', async function (assert) {
instances.session.authorized_actions =
instances.session.authorized_actions.filter((item) => item !== 'read');
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
await visit(urls.projects);
await click(`[href="${urls.targets}"]`);
@@ -447,8 +446,6 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('user can change org scope and only targets for that org will be displayed', async function (assert) {
this.stubCacheDaemonSearch(
'aliases',
'targets',
'sessions',
'targets',
'aliases',
@@ -457,6 +454,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
resource: 'targets',
func: () => [instances.target],
},
'aliases',
);
await visit(urls.scopes.global);
@@ -130,7 +130,7 @@ module('Acceptance | projects | targets | target', function (hooks) {
setDefaultClusterUrl(this);
this.ipcStub.withArgs('isCacheDaemonRunning').returns(true);
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
});
test('user can connect to a target with an address', async function (assert) {
+3 -4
View File
@@ -143,7 +143,7 @@ module('Acceptance | scopes', function (hooks) {
setDefaultClusterUrl(this);
this.ipcStub.withArgs('isCacheDaemonRunning').returns(true);
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
});
test('visiting index', async function (assert) {
@@ -213,8 +213,6 @@ module('Acceptance | scopes', function (hooks) {
test('can navigate among org scopes via header navigation', async function (assert) {
assert.expect(3);
this.stubCacheDaemonSearch(
'aliases',
'targets',
'sessions',
'targets',
'aliases',
@@ -226,6 +224,7 @@ module('Acceptance | scopes', function (hooks) {
'aliases',
'sessions',
'targets',
'aliases',
);
await visit(urls.targets);
@@ -264,7 +263,7 @@ module('Acceptance | scopes', function (hooks) {
assert.expect(1);
this.server.db.targets.remove();
this.server.db.sessions.remove();
this.stubCacheDaemonSearch('aliases', 'targets', 'sessions', 'targets');
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases');
await visit(urls.targets);