Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions addons/core/translations/resources/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1238,13 +1238,42 @@ app-token:
description: App tokens are long-lived, fine grained access tokens for authorization/authentication.
titles:
new: New App Token
tabs:
permissions: Permissions
about:
title: About this token
form:
name:
help: Name to identify this app token
description:
help: A description of this app token
approximate_last_access_time:
label: Last used at
expires_in:
label: Expires in
status:
label: Status
created_by:
label: Created by
scope:
label: Scope
ttl:
label:
0: Time to live (TTL)
1: TTL
expiration_date:
label: Expiration date
created_time:
label: Created at
tts:
label:
0: Time to stale (TTS)
1: TTS
last_used:
label: Last used
tooltip: This value is an approximate value, not an exact value.
permissions:
label: Permissions
status:
unknown: Unknown
active: Active
Expand Down
29 changes: 29 additions & 0 deletions ui/admin/app/components/description-list/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1

A reusable component for displaying a bordered container with a title
and a list of label-value pairs.

Usage example in templates:
<DescriptionList @title="Section Title" as |DL|>
<DL.Item @label="Label 1">Value 1</DL.Item>
<DL.Item @label="Label 2">Value 2</DL.Item>
</DescriptionList>

Arguments:
@title (string, optional) - Section title displayed at the top
}}

<dl class='description-list'>
{{! Optional title spanning full width }}
{{#if @title}}
<div class='description-list-title'>
<Hds::Text::Display @tag='h3' @size='300' @weight='semibold'>
{{@title}}
</Hds::Text::Display>
</div>
{{/if}}
{{! Yield DL.Item component for each label-value pair }}
{{yield (hash Item=(component 'description-list/item'))}}
</dl>
22 changes: 22 additions & 0 deletions ui/admin/app/components/description-list/item.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1

Individual item within a DescriptionList.
Displays a label (dt) above a value (dd) with 8px vertical spacing.

Arguments:
@label (string, required) - The label text for this item
Block content - The value to display (can be text, components, etc.)
}}

<div class='description-list-item'>
<dt>
<Hds::Text::Body @tag='span' @weight='semibold'>
{{@label}}
</Hds::Text::Body>
</dt>
<dd>
{{yield}}
</dd>
</div>
111 changes: 111 additions & 0 deletions ui/admin/app/components/form/app-token/read/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}

<Rose::Form>
<Hds::Form::TextInput::Field
@value={{@model.name}}
@type='text'
readOnly={{true}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'resources.app-token.form.name.help'}}</F.HelperText>
</Hds::Form::TextInput::Field>

<Hds::Form::Textarea::Field
@value={{@model.description}}
readOnly={{true}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t
'resources.app-token.form.description.help'
}}</F.HelperText>
</Hds::Form::Textarea::Field>

<DescriptionList @title={{t 'resources.app-token.about.title'}} as |DL|>
<DL.Item @label={{t 'resources.app-token.form.status.label'}}>
<Hds::Badge
@text={{this.statusBadge.text}}
@color={{this.statusBadge.color}}
/>
</DL.Item>
{{! TODO: Check where to navigate the user for global/org/proj levels }}
<DL.Item @label={{t 'resources.app-token.form.created_by.label'}}>
<Hds::Link::Inline
@route='scopes.scope.users.user'
@models={{array @model.scope.id @model.created_by_user_id}}
@icon='user'
@iconPosition='leading'
>
{{@model.created_by_user_id}}
</Hds::Link::Inline>
</DL.Item>
<DL.Item @label={{t 'resources.app-token.form.scope.label'}}>
<Hds::Link::Inline
@route='scopes.scope'
@model={{@model.scope.id}}
@icon={{this.scopeInfo.icon}}
@iconPosition='leading'
>
{{this.scopeInfo.text}}
</Hds::Link::Inline>
</DL.Item>
</DescriptionList>

<DescriptionList @title={{t 'resources.app-token.form.ttl.label.0'}} as |DL|>
<DL.Item @label={{t 'resources.app-token.form.ttl.label.1'}}>
{{#if this.ttlFormatted}}
{{format-day-year this.ttlFormatted}}
{{else}}
{{t 'labels.none'}}
{{/if}}
</DL.Item>
<DL.Item @label={{t 'resources.app-token.form.expiration_date.label'}}>
{{#if @model.expire_time}}
<Hds::Time @date={{@model.expire_time}} />
{{else}}
{{t 'labels.none'}}
{{/if}}
</DL.Item>
<DL.Item @label={{t 'resources.app-token.form.created_time.label'}}>
{{#if @model.created_time}}
<Hds::Time @date={{@model.created_time}} />
{{else}}
{{t 'labels.none'}}
{{/if}}
</DL.Item>
</DescriptionList>

<DescriptionList @title={{t 'resources.app-token.form.tts.label.0'}} as |DL|>
<DL.Item @label={{t 'resources.app-token.form.tts.label.1'}}>
{{#if this.ttsFormatted}}
{{format-day-year this.ttsFormatted}}
{{else}}
{{t 'labels.none'}}
{{/if}}
</DL.Item>
<div class='description-list-item'>
<dt class='label-with-tooltip'>
{{t 'resources.app-token.form.last_used.label'}}
<Hds::Icon
@name='info'
@isInline={{true}}
{{hds-tooltip (t 'resources.app-token.form.last_used.tooltip')}}
/>
</dt>
<dd>
{{#if @model.approximate_last_access_time}}
<Hds::Time
@date={{@model.approximate_last_access_time}}
@format='relative'
/>
{{else}}
{{t 'labels.none'}}
{{/if}}
</dd>
</div>
</DescriptionList>
</Rose::Form>
85 changes: 85 additions & 0 deletions ui/admin/app/components/form/app-token/read/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@glimmer/component';
import { service } from '@ember/service';

export default class FormAppTokenReadComponent extends Component {
@service intl;

statusConfig = {
active: { color: 'success' },
expired: { color: 'critical' },
revoked: { color: 'critical' },
stale: { color: 'critical' },
unknown: { color: 'neutral' },
};

/**
* Returns status badge configuration for app tokens
* @returns {object}
*/
get statusBadge() {
const status = this.args.model?.status;
if (!status) return { text: '', color: 'neutral' };

const config = this.statusConfig[status] || { color: 'neutral' };
return {
text: this.intl.t(`resources.app-token.status.${status}`),
color: config.color,
};
}

/**
* Returns scope information (icon, text) based on scope type
* @returns {object}
*/
get scopeInfo() {
const scope = this.args.model.scope;

if (scope.isGlobal) {
return {
icon: 'globe',
text: this.intl.t('resources.scope.types.global'),
};
}

if (scope.isOrg) {
return {
icon: 'org',
text: this.intl.t('resources.scope.types.org'),
};
}

return {
icon: 'grid',
text: this.intl.t('resources.scope.types.project'),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we are using text for the display. This is showing up as Global, Org, or Project depending on the scope level. We should be using the scope.displayName here to show the scope name or id if it has no name.

};
}

/**
* Returns formatted TTL using format-time-duration helper
* @returns {number|null} TTL in days for format-day-year helper, or null
*/
get ttlFormatted() {
const ttl = this.args.model?.TTL;
if (!ttl) return null;

// Convert milliseconds to days for format-day-year helper
return Math.floor(ttl / (1000 * 60 * 60 * 24));
}

/**
* Returns formatted TTS in days for format-day-year helper
* @returns {number|null}
*/
get ttsFormatted() {
const tts = this.args.model?.TTS;
if (!tts) return null;

// Convert milliseconds to days for format-day-year helper
return Math.floor(tts / (1000 * 60 * 60 * 24));
}
}
18 changes: 9 additions & 9 deletions ui/admin/app/controllers/scopes/scope/app-tokens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export default class ScopesScopeAppTokensIndexController extends Controller {
@tracked sortDirection;
@tracked statuses = [];

statusConfig = {
active: { color: 'success' },
expired: { color: 'critical' },
revoked: { color: 'critical' },
stale: { color: 'critical' },
unknown: { color: 'neutral' },
};

/**
* Status options for filtering
*/
Expand All @@ -49,15 +57,7 @@ export default class ScopesScopeAppTokensIndexController extends Controller {
*/
@action
getStatusBadge(status) {
const statusConfig = {
active: { color: 'success' },
expired: { color: 'critical' },
revoked: { color: 'critical' },
stale: { color: 'critical' },
unknown: { color: 'neutral' },
};

const config = statusConfig[status] || { color: 'neutral' };
const config = this.statusConfig[status] || { color: 'neutral' };
return {
text: this.intl.t(`resources.app-token.status.${status}`),
color: config.color,
Expand Down
6 changes: 5 additions & 1 deletion ui/admin/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,11 @@ Router.map(function () {
this.route('new');
this.route('policy', { path: ':policy_id' }, function () {});
});
this.route('app-tokens', function () {});
this.route('app-tokens', function () {
this.route('app-token', { path: ':token_id' }, function () {
this.route('permissions');
});
});
});
});

Expand Down
19 changes: 19 additions & 0 deletions ui/admin/app/routes/scopes/scope/app-tokens/app-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Route from '@ember/routing/route';
import { service } from '@ember/service';

export default class ScopesScopeAppTokensAppTokenRoute extends Route {
@service store;

async model(params) {
// Fetch the app-token details using the token id and scope id
return this.store.findRecord('app-token', params.token_id, {
reload: true,
adapterOptions: { scope_id: params.scope_id },
});
}
}
Loading
Loading