Skip to content

Commit

Permalink
[ui] Version Tags on the job versions page (#24013)
Browse files Browse the repository at this point in the history
* Timeline styles and their buttons modernized, and tags added

* styled but not yet functional version blocks

* Rough pass at edit/unedit UX

* Styles consolidated

* better UX around version tag crud, plus adapter and serializers

* Mirage and acceptance tests

* Modify percy to not show time-based things
  • Loading branch information
philrenaud authored Sep 25, 2024
1 parent deb4b86 commit 30c743d
Show file tree
Hide file tree
Showing 14 changed files with 590 additions and 73 deletions.
6 changes: 6 additions & 0 deletions nomad/job_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2475,5 +2475,11 @@ func (j *Job) TagVersion(args *structs.JobApplyTagRequest, reply *structs.JobTag
}

reply.Index = index
if args.Tag != nil {
reply.Name = args.Tag.Name
reply.Description = args.Tag.Description
reply.TaggedTime = args.Tag.TaggedTime
}

return nil
}
30 changes: 30 additions & 0 deletions ui/app/adapters/version-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

// @ts-check

import ApplicationAdapter from './application';
import classic from 'ember-classic-decorator';

@classic
export default class VersionTagAdapter extends ApplicationAdapter {
urlForCreateRecord(_modelName, model) {
const tagName = model.attr('name');
const jobName = model.attr('jobName');
return `${this.buildURL()}/job/${jobName}/versions/${tagName}/tag`;
}

async deleteTag(jobName, tagName) {
let deletion = this.ajax(
this.urlForDeleteRecord(jobName, tagName),
'DELETE'
);
return deletion;
}

urlForDeleteRecord(jobName, tagName) {
return `${this.buildURL()}/job/${jobName}/versions/${tagName}/tag`;
}
}
130 changes: 111 additions & 19 deletions ui/app/components/job-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,50 @@
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@ember/component';
import Component from '@glimmer/component';
import { action, computed } from '@ember/object';
import { classNames } from '@ember-decorators/component';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import messageForError from 'nomad-ui/utils/message-from-adapter-error';
import classic from 'ember-classic-decorator';

const changeTypes = ['Added', 'Deleted', 'Edited'];

@classic
@classNames('job-version', 'boxed-section')
export default class JobVersion extends Component {
version = null;
isOpen = false;
@service store;
@service notifications;
@service router;

@alias('args.version') version;
@tracked isOpen = false;
@tracked isEditing = false;
@tracked editableTag;

// Passes through to the job-diff component
verbose = true;

@service router;
constructor() {
super(...arguments);
this.initializeEditableTag();
}

initializeEditableTag() {
if (this.version.taggedVersion) {
this.editableTag = this.store.createRecord('versionTag', {
name: this.version.taggedVersion.name,
description: this.version.taggedVersion.description,
});
} else {
this.editableTag = this.store.createRecord('versionTag');
}
this.editableTag.versionNumber = this.version.number;
this.editableTag.jobName = this.version.get('job.plainId');
}

@computed('version.diff')
get changeCount() {
const diff = this.get('version.diff');
const diff = this.version.diff;
const taskGroups = diff.TaskGroups || [];

if (!diff) {
Expand All @@ -44,43 +64,115 @@ export default class JobVersion extends Component {

@computed('version.{number,job.version}')
get isCurrent() {
return this.get('version.number') === this.get('version.job.version');
return this.version.number === this.version.get('job.version');
}

@action
toggleDiff() {
this.toggleProperty('isOpen');
this.isOpen = !this.isOpen;
}

@task(function* () {
try {
const versionBeforeReversion = this.get('version.job.version');

const versionBeforeReversion = this.version.get('job.version');
yield this.version.revertTo();
yield this.version.job.reload();

const versionAfterReversion = this.get('version.job.version');
yield this.version.get('job').reload();

const versionAfterReversion = this.version.get('job.version');
if (versionBeforeReversion === versionAfterReversion) {
this.handleError({
this.args.handleError({
level: 'warn',
title: 'Reversion Had No Effect',
description:
'Reverting to an identical older version doesn’t produce a new version',
});
} else {
const job = this.get('version.job');
const job = this.version.get('job');
this.router.transitionTo('jobs.job.index', job.get('idWithNamespace'));
}
} catch (e) {
this.handleError({
this.args.handleError({
level: 'danger',
title: 'Could Not Revert',
description: messageForError(e, 'revert'),
});
}
})
revertTo;

@action
handleKeydown(event) {
if (event.key === 'Escape') {
this.cancelEditTag();
}
}

@action
toggleEditTag() {
this.isEditing = !this.isEditing;
}

@action
async saveTag(event) {
event.preventDefault();
try {
if (!this.editableTag.name) {
this.notifications.add({
title: 'Error Tagging Job Version',
message: 'Tag name is required',
color: 'critical',
});
return;
}
const savedTag = await this.editableTag.save();
this.version.taggedVersion = savedTag;
this.version.taggedVersion.setProperties({
...savedTag.toJSON(),
});
this.initializeEditableTag();
this.isEditing = false;

this.notifications.add({
title: 'Job Version Tagged',
color: 'success',
});
} catch (error) {
console.log('error tagging job version', error);
this.notifications.add({
title: 'Error Tagging Job Version',
message: messageForError(error),
color: 'critical',
});
}
}

@action
cancelEditTag() {
this.isEditing = false;
this.initializeEditableTag();
}

@action
async deleteTag() {
try {
await this.store
.adapterFor('version-tag')
.deleteTag(this.editableTag.jobName, this.editableTag.name);
this.notifications.add({
title: 'Job Version Un-Tagged',
color: 'success',
});
this.version.taggedVersion = null;
this.initializeEditableTag();
this.isEditing = false;
} catch (error) {
this.notifications.add({
title: 'Error Un-Tagging Job Version',
message: messageForError(error),
color: 'critical',
});
}
}
}

const flatten = (accumulator, array) => accumulator.concat(array);
Expand Down
2 changes: 2 additions & 0 deletions ui/app/models/job-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import Model from '@ember-data/model';
import { fragment } from 'ember-data-model-fragments/attributes';
import { attr, belongsTo } from '@ember-data/model';

export default class JobVersion extends Model {
Expand All @@ -12,6 +13,7 @@ export default class JobVersion extends Model {
@attr('date') submitTime;
@attr('number') number;
@attr() diff;
@fragment('version-tag') taggedVersion;

revertTo() {
return this.store.adapterFor('job-version').revertTo(this);
Expand Down
15 changes: 15 additions & 0 deletions ui/app/models/version-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';

export default class VersionTagModel extends Fragment {
@attr() name;
@attr() description;
@attr() taggedTime;
@attr('number') versionNumber;
@attr('string') jobName;
}
7 changes: 7 additions & 0 deletions ui/app/serializers/job-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export default class JobVersionSerializer extends ApplicationSerializer {
number: 'Version',
};

normalize(typeHash, hash) {
if (hash.TaggedVersion) {
hash.TaggedVersion.VersionNumber = hash.Version;
}
return super.normalize(typeHash, hash);
}

normalizeFindHasManyResponse(store, modelClass, hash, id, requestType) {
const zippedVersions = hash.Versions.map((version, index) =>
assign({}, version, {
Expand Down
18 changes: 18 additions & 0 deletions ui/app/serializers/version-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

// @ts-check
import ApplicationSerializer from './application';
import { inject as service } from '@ember/service';

export default class VersionTagSerializer extends ApplicationSerializer {
@service store;

serialize(snapshot, options) {
const hash = super.serialize(snapshot, options);
hash.Version = hash.VersionNumber;
return hash;
}
}
75 changes: 75 additions & 0 deletions ui/app/styles/components/timeline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,80 @@
> .boxed-section {
margin-bottom: 0;
}

.job-version {
margin-bottom: 0;
& > .boxed-section {
box-shadow: var(--token-surface-high-box-shadow);
border-radius: 0.25rem;
header,
footer {
border: none;
padding: 0.75rem;
}

footer {
background-color: var(--token-color-surface-faint);
border-top: 1px solid var(--token-color-border-faint);
display: grid;
grid-template-columns: auto auto;
align-items: center;
gap: 0.5rem;
& > .tag-options {
justify-self: start;
display: grid;
grid-template-areas: 'name description save cancel delete';
grid-template-columns: auto 1fr auto auto auto;
gap: 0.5rem;
align-items: center;

// Match the height of HDS:Button's @size="small"
input {
padding: 0.375rem 0.6875rem;
line-height: 100%;
}

.tag-button-primary {
grid-area: name;
background-color: var(--token-color-surface-highlight);
color: var(--token-color-foreground-highlight-on-surface);
border-color: var(--token-color-foreground-highlight);
&:focus:before {
border-color: var(--token-color-foreground-highlight);
}
&:hover {
background-color: var(--token-color-border-highlight);
}
}
.tag-button-secondary {
grid-area: name;
}
.tag-description {
grid-area: description;
font-style: italic;
font-size: 0.875rem;
white-space: no-wrap;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
max-width: 100%;
}
& > .tag-name {
grid-area: name;
}
}
& > .version-options {
justify-self: end;
}

&.editing {
grid-template-columns: 1fr;
& > .tag-options {
width: 100%;
}
}
}
}
}
}
}
Loading

0 comments on commit 30c743d

Please sign in to comment.