feat: 🎸 Add loading indicators for initial loads of resources (#2074)

* feat: 🎸 Add loading indicators for initial loads of resources

* refactor: 💡 Use HDS application state component

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12025
This commit is contained in:
Zhihe Li committed 2024-01-16 10:17:46 -05:00
1 parent 16a054519f
commit ba0c7d5683
12 files changed
+532 -499

No files matched your search

+27
View File
@@ -20,3 +20,30 @@
}
}
}
// Search & Filtering
.search-filtering {
.hds-application-state {
margin-top: 3rem;
}
.hds-segmented-group {
margin-bottom: 1rem;
.hds-dropdown-toggle-button__text {
white-space: nowrap;
}
}
.hds-table {
margin-bottom: 1rem;
}
}
// Flyout with table
.flyout-with-table {
.copyable-content a,
button {
color: var(--token-color-foreground-primary);
}
}
@@ -14,3 +14,6 @@ active-public: Public
recording: Recording
completed: Completed
failed: Failed
loading:
title: Loading all items...
description: All of your {resource} are still loading. Please wait while we finish loading everything on screen.
-12
View File
@@ -8,7 +8,6 @@ import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { A } from '@ember/array';
import { getOwner } from '@ember/application';
import { loading } from 'ember-loading';
export default class ApplicationRoute extends Route {
// =services
@@ -78,17 +77,6 @@ export default class ApplicationRoute extends Route {
this.invalidateSession();
}
/**
* Hooks into ember-loading to kick off loading indicator in the
* application template.
* @return {boolean} always returns true
*/
@action
@loading
loading() {
return true;
}
/**
* Invalidates the session if a 401 error occurs and returns false to
* prevent further error handling.
@@ -5,6 +5,7 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
// =services
@@ -40,6 +41,20 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
// =methods
/**
* Event to determine whether the loading template should be shown.
* Only show the loading template during initial loads or when transitioning
* from different routes. Don't show it when a user is just searching or
* filtering on the same page as it can be jarring.
* @param transition
* @returns {boolean}
*/
@action
loading(transition) {
const from = transition.from?.name;
return from !== 'scopes.scope.projects.sessions.index';
}
/**
* Loads queried sessions, the total number of sessions, all sessions,
* all targets and all projects for filtering options for the current user.
@@ -9,6 +9,7 @@ import {
STATUS_SESSION_ACTIVE,
STATUS_SESSION_PENDING,
} from 'api/models/session';
import { action } from '@ember/object';
export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
// =services
@@ -51,6 +52,20 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
// =methods
/**
* Event to determine whether the loading template should be shown.
* Only show the loading template during initial loads or when transitioning
* from different routes. Don't show it when a user is just searching or
* filtering on the same page as it can be jarring.
* @param transition
* @returns {boolean}
*/
@action
loading(transition) {
const from = transition.from?.name;
return from !== 'scopes.scope.projects.targets.index';
}
/**
* If arriving here unauthenticated, redirect to index for further processing.
*/
-27
View File
@@ -684,30 +684,3 @@ $chevron-down: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231
color: var(--token-color-foreground-faint);
}
}
// Search & Filtering
.search-filtering {
.hds-application-state {
margin-top: 3rem;
}
.hds-segmented-group {
margin-bottom: 1rem;
.hds-dropdown-toggle-button__text {
white-space: nowrap;
}
}
.hds-table {
margin-bottom: 1rem;
}
}
// Flyout with table
.flyout-with-table {
.copyable-content a,
button {
color: var(--token-color-foreground-primary);
}
}
@@ -3,4 +3,16 @@
SPDX-License-Identifier: MPL-2.0
}}
{{outlet}}
<Rose::Layout::Page as |page|>
<page.header>
<h2>
{{t 'resources.session.title_plural'}}
<DocLink @doc='sessions' @iconSize='large' />
</h2>
<p>{{t 'resources.session.description'}}</p>
</page.header>
<page.body class='search-filtering'>
{{outlet}}
</page.body>
</Rose::Layout::Page>
@@ -3,191 +3,175 @@
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Layout::Page as |page|>
<page.header>
<h2>
{{t 'resources.session.title_plural'}}
<DocLink @doc='sessions' @iconSize='large' />
</h2>
<p>{{t 'resources.session.description'}}</p>
</page.header>
<page.body class='search-filtering'>
{{#if (or @model.sessions @model.allSessions)}}
<Hds::SegmentedGroup as |S|>
<S.Generic>
<Dropdown
@name={{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={{t 'form.status.label'}}
@itemOptions={{this.sessionStatusOptions}}
@checkedItems={{this.status}}
@applyFilter={{fn this.applyFilter 'status'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
@name={{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>
<FilterTags @filters={{this.filters}} />
{{#if @model.sessions}}
<Hds::Table
@model={{this.sortedSessions}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.target.title'))
(hash label=(t 'form.proxy_url.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.status.label'))
(hash label='')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if B.data.isAvailable}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
@color='success'
/>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<LinkTo
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-session-detail-link
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</LinkTo>
</Copyable>
{{else}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
/>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code
data-test-session-id-copy
>{{B.data.id}}</Hds::Text::Code>
</Copyable>
{{/if}}
</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
is canceled or in the process thereof, the local proxy
may no longer be used.
2) The session actually has proxy information associated
with it. It's possible for the user to start sessions
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}}
<Copyable
@text={{B.data.proxy}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.proxy}}</Hds::Text::Code>
</Copyable>
{{/if}}
{{/if}}
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<SessionStatus @model={{B.data}} />
</B.Td>
<B.Td>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
@text={{t 'actions.cancel'}}
@color='secondary'
{{on 'click' (route-action 'cancelSession' B.data)}}
data-test-session-cancel-button
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
{{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='sessions'}}
/>
</Hds::ApplicationState>
{{/if}}
{{else}}
<Hds::ApplicationState data-test-no-sessions as |A|>
<A.Header
@title={{t 'resources.session.messages.none-friendly.title'}}
/>
<A.Body
@text={{t 'resources.session.messages.none-friendly.description'}}
/>
</Hds::ApplicationState>
{{/if}}
</page.body>
</Rose::Layout::Page>
{{#if (or @model.sessions @model.allSessions)}}
<Hds::SegmentedGroup as |S|>
<S.Generic>
<Dropdown
@name={{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={{t 'form.status.label'}}
@itemOptions={{this.sessionStatusOptions}}
@checkedItems={{this.status}}
@applyFilter={{fn this.applyFilter 'status'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
@name={{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>
<FilterTags @filters={{this.filters}} />
{{#if @model.sessions}}
<Hds::Table
@model={{this.sortedSessions}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.target.title'))
(hash label=(t 'form.proxy_url.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.status.label'))
(hash label='')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if B.data.isAvailable}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
@color='success'
/>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<LinkTo
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-session-detail-link
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</LinkTo>
</Copyable>
{{else}}
<Hds::Badge
@text={{t 'resources.session.title'}}
@icon='entry-point'
@isIconOnly={{true}}
/>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code
data-test-session-id-copy
>{{B.data.id}}</Hds::Text::Code>
</Copyable>
{{/if}}
</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
is canceled or in the process thereof, the local proxy
may no longer be used.
2) The session actually has proxy information associated
with it. It's possible for the user to start sessions
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}}
<Copyable
@text={{B.data.proxy}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.proxy}}</Hds::Text::Code>
</Copyable>
{{/if}}
{{/if}}
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<SessionStatus @model={{B.data}} />
</B.Td>
<B.Td>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
@text={{t 'actions.cancel'}}
@color='secondary'
{{on 'click' (route-action 'cancelSession' B.data)}}
data-test-session-cancel-button
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
{{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='sessions'}}
/>
</Hds::ApplicationState>
{{/if}}
{{else}}
<Hds::ApplicationState data-test-no-sessions as |A|>
<A.Header @title={{t 'resources.session.messages.none-friendly.title'}} />
<A.Body
@text={{t 'resources.session.messages.none-friendly.description'}}
/>
</Hds::ApplicationState>
{{/if}}
@@ -0,0 +1,9 @@
<Hds::ApplicationState as |A|>
<A.Header @title={{t 'states.loading.title'}} @icon='loading' />
<A.Body
@text={{t
'states.loading.description'
resource=(t 'resources.session.title_plural')
}}
/>
</Hds::ApplicationState>
@@ -5,4 +5,16 @@
{{page-title (t 'resources.target.title_plural')}}
{{outlet}}
<Rose::Layout::Page as |page|>
<page.header>
<h2>
{{t 'resources.target.title_plural'}}
<DocLink @doc='targets' @iconSize='large' />
</h2>
<p>{{t 'resources.target.description'}}</p>
</page.header>
<page.body class='search-filtering'>
{{outlet}}
</page.body>
</Rose::Layout::Page>
@@ -3,278 +3,264 @@
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Layout::Page as |page|>
{{#if this.showFilters}}
<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={{t 'resources.target.titles.active-sessions'}}
@itemOptions={{this.availableSessionOptions}}
@checkedItems={{this.availableSessions}}
@applyFilter={{fn this.applyFilter 'availableSessions'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
@name={{t 'form.type.label'}}
@itemOptions={{this.targetTypeOptions}}
@checkedItems={{this.types}}
@applyFilter={{fn this.applyFilter 'types'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
@name={{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>
<FilterTags @filters={{this.filters}} />
{{#if @model.targets}}
<Hds::Table
@model={{@model.targets}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.target.titles.active-sessions'))
(hash label=(t 'form.type.label'))
(hash label=(t 'resources.project.title'))
(hash label='')
}}
@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>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</Copyable>
</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>
<Copyable
@text={{B.data.project.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.project.id}}</Hds::Text::Code>
</Copyable>
</div>
{{#if B.data.project.name}}
<Hds::Badge @color='neutral' @text={{B.data.project.name}} />
{{/if}}
</B.Td>
<B.Td>
{{#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}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
<page.header>
<h2>
{{t 'resources.target.title_plural'}}
<DocLink @doc='targets' @iconSize='large' />
</h2>
<p>{{t 'resources.target.description'}}</p>
</page.header>
<page.body class='search-filtering'>
{{#if this.showFilters}}
<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={{t 'resources.target.titles.active-sessions'}}
@itemOptions={{this.availableSessionOptions}}
@checkedItems={{this.availableSessions}}
@applyFilter={{fn this.applyFilter 'availableSessions'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
@name={{t 'form.type.label'}}
@itemOptions={{this.targetTypeOptions}}
@checkedItems={{this.types}}
@applyFilter={{fn this.applyFilter 'types'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
@name={{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>
<FilterTags @filters={{this.filters}} />
{{#if @model.targets}}
<Hds::Table
@model={{@model.targets}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.target.titles.active-sessions'))
(hash label=(t 'form.type.label'))
(hash label=(t 'resources.project.title'))
(hash label='')
}}
@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>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
{{/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='targets'
}}
/>
</Hds::ApplicationState>
{{/if}}
{{/if}}
{{#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'}} />
</Hds::ApplicationState>
{{/if}}
{{#if (and this.selectedTarget this.selectedTarget.isActive)}}
<Hds::Flyout
@onClose={{fn this.selectTarget null}}
class='flyout-with-table'
data-test-targets-sessions-flyout
as |M|
>
<M.Header>
{{t
'resources.target.titles.sessions-flyout'
targetDisplayName=this.selectedTarget.displayName
}}
</M.Header>
<M.Body>
<Hds::Table
@model={{this.sortedTargetSessions}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.proxy_url.label'))
(hash label='')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<LinkTo
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-targets-session-detail-link={{B.data.id}}
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</Copyable>
</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>
<Copyable
@text={{B.data.project.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.project.id}}</Hds::Text::Code>
</Copyable>
</div>
{{#if B.data.project.name}}
<Hds::Badge @color='neutral' @text={{B.data.project.name}} />
{{/if}}
</B.Td>
<B.Td>
{{#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}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
</LinkTo>
</Copyable>
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
{{B.data.proxy}}
</B.Td>
<B.Td>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
data-test-session-flyout-cancel-button={{B.data.id}}
@text={{t 'resources.session.actions.end'}}
@icon='x'
@isIconOnly={{true}}
@color='secondary'
{{on 'click' (fn this.cancelSession B.data)}}
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{#if this.showFlyoutViewMoreLink}}
<Hds::Link::Standalone
@text={{t 'resources.target.actions.view-more-sessions'}}
@route='scopes.scope.projects.sessions'
@query={{this.viewMoreLinkQueryParams}}
@icon='arrow-right'
@iconPosition='trailing'
/>
{{/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='targets'
}}
/>
</Hds::ApplicationState>
{{/if}}
{{/if}}
{{#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'}} />
</Hds::ApplicationState>
{{/if}}
{{#if (and this.selectedTarget this.selectedTarget.isActive)}}
<Hds::Flyout
@onClose={{fn this.selectTarget null}}
class='flyout-with-table'
data-test-targets-sessions-flyout
as |M|
>
<M.Header>
{{t
'resources.target.titles.sessions-flyout'
targetDisplayName=this.selectedTarget.displayName
}}
</M.Header>
<M.Body>
<Hds::Table
@model={{this.sortedTargetSessions}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.proxy_url.label'))
(hash label='')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<LinkTo
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-targets-session-detail-link={{B.data.id}}
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</LinkTo>
</Copyable>
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
{{B.data.proxy}}
</B.Td>
<B.Td>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
data-test-session-flyout-cancel-button={{B.data.id}}
@text={{t 'resources.session.actions.end'}}
@icon='x'
@isIconOnly={{true}}
@color='secondary'
{{on 'click' (fn this.cancelSession B.data)}}
/>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{#if this.showFlyoutViewMoreLink}}
<Hds::Link::Standalone
@text={{t 'resources.target.actions.view-more-sessions'}}
@route='scopes.scope.projects.sessions'
@query={{this.viewMoreLinkQueryParams}}
@icon='arrow-right'
@iconPosition='trailing'
/>
{{/if}}
</M.Body>
</Hds::Flyout>
{{/if}}
</page.body>
</Rose::Layout::Page>
</M.Body>
</Hds::Flyout>
{{/if}}
@@ -0,0 +1,9 @@
<Hds::ApplicationState as |A|>
<A.Header @title={{t 'states.loading.title'}} @icon='loading' />
<A.Body
@text={{t
'states.loading.description'
resource=(t 'resources.target.title_plural')
}}
/>
</Hds::ApplicationState>