diff --git a/.changelog/1840.txt b/.changelog/1840.txt new file mode 100644 index 00000000000..52cec59dfce --- /dev/null +++ b/.changelog/1840.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Incorporate pushed artifacts into build display +``` \ No newline at end of file diff --git a/ui/app/components/app-card/build.hbs b/ui/app/components/app-card/build.hbs index 3d3af70ae85..37eb555702d 100644 --- a/ui/app/components/app-card/build.hbs +++ b/ui/app/components/app-card/build.hbs @@ -9,28 +9,25 @@ > v{{@model.sequence}} - + <:meta-secondary> - - - - {{#if (eq @model.status.state 2)}} - Built with - {{component-name @model.component.name}} - in - {{date-format-distance @model.status.startTime.seconds @model.status.completeTime.seconds }} - {{else if (eq @model.status.state 3)}} - Failed to build with - {{component-name @model.component.name}} - {{else}} - Building with - {{component-name @model.component.name}} - {{/if}} - + {{#let (or @model.pushedArtifact @model) as |operation|}} + + + {{t + (concat + "build_status" + ".type-" operation.component.type + ".state-" operation.status.state + ) + }} + {{component-name operation.component.name}} + + {{/let}} diff --git a/ui/app/components/app-item/build.hbs b/ui/app/components/app-item/build.hbs index b4bc0a2d703..4e7dc96f9f4 100644 --- a/ui/app/components/app-item/build.hbs +++ b/ui/app/components/app-item/build.hbs @@ -1,34 +1,39 @@ -
  • +
  • -

    - v{{@build.sequence}} -

    + v{{@build.sequence}} - - {{if (eq @model.status.state 1) 'Building' 'Built'}} with - {{titleize @build.component.name}} - {{#if (eq @build.status.state 1)}} - (Started {{date-format-distance-to-now @build.status.startTime.seconds }}) - {{else}} - {{date-format-distance-to-now @build.status.completeTime.seconds }} - {{/if}} - + {{#let (or @build.pushedArtifact @build) as |operation|}} + + + + {{t + (concat + "build_status" + ".type-" operation.component.type + ".state-" operation.status.state + ) + }} + {{component-name operation.component.name}} + + + {{/let}}
    - {{#if (eq @build.status.state 1)}} - + + {{#if (and (eq @build.status.state 2) (eq @build.pushedArtifact.status.state 2))}} + - Building... - - {{else if (eq @build.status.state 2)}} - - - Built in {{date-format-distance @build.status.startTime.seconds @build.status.completeTime.seconds }} - - {{else if (eq @build.status.state 3)}} - - - Build failed + + {{t "app_item_build.built_in" + duration=(date-format-distance + @build.status.startTime.seconds + @build.status.completeTime.seconds + ) + }} + {{/if}}
  • \ No newline at end of file diff --git a/ui/app/components/operation-status-indicator.hbs b/ui/app/components/operation-status-indicator.hbs index 3fec964532c..61ad3d126a3 100644 --- a/ui/app/components/operation-status-indicator.hbs +++ b/ui/app/components/operation-status-indicator.hbs @@ -1,3 +1,21 @@ +{{!-- + + ## Usage + + + + If you would like the indicator to match the surrounding typography, + pass `@matchTypography={{true}}`: + + + +--}} + {{#let (hash state=(or @@ -14,10 +32,11 @@ as |vars| }} b.id === pushedArtifact.buildId); + if (build) { + build.pushedArtifact = pushedArtifact; + } + } + } +} diff --git a/ui/app/routes/workspace/projects/project/app/build.ts b/ui/app/routes/workspace/projects/project/app/build.ts index 35acaeb30d4..de597dad4a5 100644 --- a/ui/app/routes/workspace/projects/project/app/build.ts +++ b/ui/app/routes/workspace/projects/project/app/build.ts @@ -1,12 +1,19 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import ApiService from 'waypoint/services/api'; -import { Ref, GetBuildRequest } from 'waypoint-pb'; +import { Ref, GetBuildRequest, Build, PushedArtifact } from 'waypoint-pb'; import { Model as AppRouteModel } from '../app'; interface BuildModelParams { sequence: number; } + +interface WithPushedArtifact { + pushedArtifact?: PushedArtifact.AsObject; +} + +type BuildWithArtifact = Build.AsObject & WithPushedArtifact; + export default class BuildDetail extends Route { @service api!: ApiService; @@ -28,15 +35,23 @@ export default class BuildDetail extends Route { async model(params: BuildModelParams) { // Setup the build request - let { builds } = this.modelFor('workspace.projects.project.app'); - let { id: build_id } = builds.find((obj) => obj.sequence === Number(params.sequence)); + let { builds } = this.modelFor('workspace.projects.project.app') as AppRouteModel; + let buildFromAppRoute = builds.find((obj) => obj.sequence === Number(params.sequence)); + + if (!buildFromAppRoute) { + throw new Error('Build not found'); + } let ref = new Ref.Operation(); - ref.setId(build_id); + ref.setId(buildFromAppRoute.id); let req = new GetBuildRequest(); req.setRef(ref); let build = await this.api.client.getBuild(req, this.api.WithMeta()); - return build.toObject(); + let result: BuildWithArtifact = build.toObject(); + + result.pushedArtifact = buildFromAppRoute.pushedArtifact; + + return result; } } diff --git a/ui/app/services/api.ts b/ui/app/services/api.ts index 55b41050597..f8ec9c7cf4f 100644 --- a/ui/app/services/api.ts +++ b/ui/app/services/api.ts @@ -19,6 +19,8 @@ import { ListStatusReportsRequest, ListStatusReportsResponse, GetLatestStatusReportRequest, + ListPushedArtifactsRequest, + PushedArtifact, } from 'waypoint-pb'; import config from 'waypoint/config/environment'; @@ -84,6 +86,25 @@ export default class ApiService extends Service { return resp.getBuildsList().map((d) => d.toObject()); } + async listPushedArtifacts( + wsRef: Ref.Workspace, + appRef: Ref.Application + ): Promise { + let request = new ListPushedArtifactsRequest(); + + request.setApplication(appRef); + request.setWorkspace(wsRef); + + // TODO(jgwhite): request.setIncludeBuild + // TODO(jgwhite): request.setOrder + // TODO(jgwhite): request.setStatusList + + let response = await this.client.listPushedArtifacts(request, this.WithMeta()); + let result = response.getArtifactsList().map((pa) => pa.toObject()); + + return result; + } + async listReleases(wsRef: Ref.Workspace, appRef: Ref.Application): Promise { let req = new ListReleasesRequest(); req.setWorkspace(wsRef); diff --git a/ui/app/styles/components/badge.scss b/ui/app/styles/components/badge.scss index cf4d780548c..12f9c169592 100644 --- a/ui/app/styles/components/badge.scss +++ b/ui/app/styles/components/badge.scss @@ -27,6 +27,13 @@ background: rgb(var(--warning)); } + &--info { + @media (prefers-color-scheme: light) { + color: color.$ui-cool-gray-700; + background: color.$ui-cool-gray-100; + } + } + .icon { width: scale.$base; height: scale.$base; diff --git a/ui/app/styles/components/operation-status-indicator.scss b/ui/app/styles/components/operation-status-indicator.scss index 853ca3bd9d2..56ca87f8bdd 100644 --- a/ui/app/styles/components/operation-status-indicator.scss +++ b/ui/app/styles/components/operation-status-indicator.scss @@ -6,4 +6,9 @@ &--error { color: rgb(var(--error-text)); } + + &--match-typography { + font-size: inherit; + font-weight: inherit; + } } diff --git a/ui/app/templates/workspace/projects/project/app/build.hbs b/ui/app/templates/workspace/projects/project/app/build.hbs index 2a2605baaa5..fb9652d25c6 100644 --- a/ui/app/templates/workspace/projects/project/app/build.hbs +++ b/ui/app/templates/workspace/projects/project/app/build.hbs @@ -1,49 +1,37 @@ - -
    -

    - v{{@model.sequence}} -

    - - - {{if (eq @model.status.state 1) 'Building' 'Built'}} with - {{titleize @model.component.name}} - {{#if (eq @model.status.state 1)}} - (Started {{date-format-distance-to-now @model.status.startTime.seconds }}) - {{else}} - {{date-format-distance-to-now @model.status.completeTime.seconds }} - {{/if}} - - -
    -
    - -
    -
    +{{#let (or @model.pushedArtifact @model) as |operation|}} + +
    +

    + v{{@model.sequence}} +

    + + + + + {{t + (concat + "build_status" + ".type-" operation.component.type + ".state-" operation.status.state + ) + }} + {{component-name operation.component.name}} + + +
    +
    + +
    +
    -
    -
    - {{#if (eq @model.status.state 1)}} - - - Build running... - - {{else if (eq @model.status.state 2)}} - - - Built in {{date-format-distance @model.status.startTime.seconds @model.status.completeTime.seconds }} - - {{else if (eq @model.status.state 3)}} - - - - Build failed - {{#if @model.status.error.message}} - : {{@model.status.error.message}} - {{/if}} - - - {{/if}} +
    +
    + +
    -
    +{{/let}} diff --git a/ui/mirage/config.ts b/ui/mirage/config.ts index 85a6a531da6..1239f82f50f 100644 --- a/ui/mirage/config.ts +++ b/ui/mirage/config.ts @@ -13,6 +13,7 @@ import * as versionInfo from './services/version-info'; import * as statusReport from './services/status-report'; import * as job from './services/job'; import * as log from './services/log'; +import * as pushedArtifact from './services/pushed-artifact'; export default function (this: Server) { this.namespace = 'hashicorp.waypoint.Waypoint'; @@ -48,6 +49,7 @@ export default function (this: Server) { this.post('/GetLatestStatusReport', statusReport.getLatest); this.post('/GetJobStream', job.stream); this.post('/GetLogStream', log.stream); + this.post('/ListPushedArtifacts', pushedArtifact.list); if (!Ember.testing) { // Pass through all other requests diff --git a/ui/mirage/factories/build.ts b/ui/mirage/factories/build.ts index eb950c30abc..07218ea90d8 100644 --- a/ui/mirage/factories/build.ts +++ b/ui/mirage/factories/build.ts @@ -11,6 +11,9 @@ export default Factory.extend({ server.schema.workspaces.findBy({ name: 'default' }) || server.create('workspace', 'default'); build.update('workspace', workspace); } + + build.pushedArtifact?.update('application', build.application); + build.pushedArtifact?.update('workspace', build.workspace); }, random: trait({ @@ -20,6 +23,7 @@ export default Factory.extend({ }), component: association('builder', 'with-random-name'), status: association('random'), + pushedArtifact: association('random'), }), docker: trait({ diff --git a/ui/mirage/factories/component.ts b/ui/mirage/factories/component.ts index 836012ab821..65983f7396b 100644 --- a/ui/mirage/factories/component.ts +++ b/ui/mirage/factories/component.ts @@ -48,6 +48,10 @@ export default Factory.extend({ name: 'kubernetes-apply', }), + 'aws-ecr': trait({ + name: 'aws-ecr', + }), + 'with-random-name': trait({ afterCreate(component) { component.update('name', randomNameForType(component.type)); diff --git a/ui/mirage/factories/project.ts b/ui/mirage/factories/project.ts index a9c856c8009..02095bc7cf3 100644 --- a/ui/mirage/factories/project.ts +++ b/ui/mirage/factories/project.ts @@ -93,30 +93,37 @@ export default Factory.extend({ server.create('build', 'docker', 'days-old-success', { application, sequence: 1, + pushedArtifact: server.create('pushed-artifact', 'docker', 'days-old-success'), }), server.create('build', 'docker', 'days-old-success', { application, sequence: 2, + pushedArtifact: server.create('pushed-artifact', 'docker', 'days-old-success'), }), server.create('build', 'docker', 'hours-old-success', { application, sequence: 3, + pushedArtifact: server.create('pushed-artifact', 'docker', 'hours-old-success'), }), server.create('build', 'docker', 'hours-old-success', { application, sequence: 4, + pushedArtifact: server.create('pushed-artifact', 'docker', 'hours-old-success'), }), server.create('build', 'docker', 'minutes-old-success', { application, sequence: 5, + pushedArtifact: server.create('pushed-artifact', 'docker', 'minutes-old-success'), }), server.create('build', 'docker', 'minutes-old-success', { application, sequence: 6, + pushedArtifact: server.create('pushed-artifact', 'docker', 'minutes-old-success'), }), server.create('build', 'docker', 'seconds-old-success', { application, sequence: 7, + pushedArtifact: server.create('pushed-artifact', 'docker', 'seconds-old-success'), }), ]; diff --git a/ui/mirage/factories/pushed-artifact.ts b/ui/mirage/factories/pushed-artifact.ts new file mode 100644 index 00000000000..a8a2418e4f1 --- /dev/null +++ b/ui/mirage/factories/pushed-artifact.ts @@ -0,0 +1,48 @@ +import { Factory, association, trait } from 'ember-cli-mirage'; +import { fakeId } from '../utils'; + +export default Factory.extend({ + id: () => fakeId(), + sequence: (i) => i + 1, + + afterCreate(pushedArtifact, server) { + if (!pushedArtifact.workspace) { + let workspace = + server.schema.workspaces.findBy({ name: 'default' }) || server.create('workspace', 'default'); + pushedArtifact.update('workspace', workspace); + } + }, + + random: trait({ + component: association('registry', 'with-random-name'), + status: association('random'), + }), + + docker: trait({ + component: association('registry', 'docker'), + }), + + 'aws-ecr': trait({ + component: association('registry', 'aws-ecr'), + }), + + 'seconds-old-success': trait({ + status: association('random', 'success', 'seconds-old'), + }), + + 'seconds-old-error': trait({ + status: association('random', 'error', 'seconds-old'), + }), + + 'minutes-old-success': trait({ + status: association('random', 'success', 'minutes-old'), + }), + + 'hours-old-success': trait({ + status: association('random', 'success', 'hours-old'), + }), + + 'days-old-success': trait({ + status: association('random', 'success', 'days-old'), + }), +}); diff --git a/ui/mirage/factories/status.ts b/ui/mirage/factories/status.ts index 54c4af6519e..c085a361692 100644 --- a/ui/mirage/factories/status.ts +++ b/ui/mirage/factories/status.ts @@ -20,6 +20,10 @@ export default Factory.extend({ state: 'SUCCESS', }), + error: trait({ + state: 'ERROR', + }), + 'seconds-old': trait({ completeTime: () => new Date(), }), diff --git a/ui/mirage/models/build.ts b/ui/mirage/models/build.ts index fc10da60172..24422d9d76c 100644 --- a/ui/mirage/models/build.ts +++ b/ui/mirage/models/build.ts @@ -7,6 +7,7 @@ export default Model.extend({ component: belongsTo({ inverse: 'owner' }), status: belongsTo({ inverse: 'owner' }), deployments: hasMany(), + pushedArtifact: belongsTo({ inverse: 'build' }), toProtobuf(): Build { let result = new Build(); diff --git a/ui/mirage/models/pushed-artifact.ts b/ui/mirage/models/pushed-artifact.ts new file mode 100644 index 00000000000..6c36c9bfa35 --- /dev/null +++ b/ui/mirage/models/pushed-artifact.ts @@ -0,0 +1,28 @@ +import { Model, belongsTo } from 'miragejs'; +import { PushedArtifact } from 'waypoint-pb'; + +export default Model.extend({ + application: belongsTo(), + build: belongsTo({ inverse: 'pushedArtifact' }), + component: belongsTo({ inverse: 'owner' }), + status: belongsTo({ inverse: 'owner' }), + workspace: belongsTo(), + + toProtobuf(): PushedArtifact { + let result = new PushedArtifact(); + + result.setApplication(this.application?.toProtobufRef()); + // TODO: result.setArtifact + result.setBuild(this.build?.toProtobuf()); + result.setBuildId(this.build?.id); + result.setComponent(this.component?.toProtobuf()); + result.setId(this.id); + result.setJobId(this.jobId); + result.setSequence(this.sequence); + result.setStatus(this.status?.toProtobuf()); + result.setTemplateData(this.templateData); + result.setWorkspace(this.workspace?.toProtobufRef()); + + return result; + }, +}); diff --git a/ui/mirage/services/pushed-artifact.ts b/ui/mirage/services/pushed-artifact.ts new file mode 100644 index 00000000000..88e16377525 --- /dev/null +++ b/ui/mirage/services/pushed-artifact.ts @@ -0,0 +1,25 @@ +import { Request, Response } from 'miragejs'; +import { ListPushedArtifactsRequest, ListPushedArtifactsResponse } from 'waypoint-pb'; +import { decode } from '../helpers/protobufs'; + +export function list(schema: any, request: Request): Response { + let requestMsg = decode(ListPushedArtifactsRequest, request.requestBody); + let projectName = requestMsg.getApplication().getProject(); + let appName = requestMsg.getApplication().getApplication(); + let workspaceName = requestMsg.getWorkspace().getWorkspace(); + let project = schema.projects.findBy({ name: projectName }); + let application = schema.applications.findBy({ name: appName, projectId: project.id }); + let workspace = schema.workspaces.findBy({ name: workspaceName }); + let pushedArtifacts = schema.pushedArtifacts.where({ + applicationId: application?.id, + workspaceId: workspace?.id, + }); + let pushedArtifactProtobufs = pushedArtifacts.models.map((b) => b.toProtobuf()); + let resp = new ListPushedArtifactsResponse(); + + pushedArtifactProtobufs.sort((a, b) => b.getSequence() - a.getSequence()); + + resp.setArtifactsList(pushedArtifactProtobufs); + + return this.serialize(resp, 'application'); +} diff --git a/ui/tests/integration/components/app-item/build-test.ts b/ui/tests/integration/components/app-item/build-test.ts new file mode 100644 index 00000000000..102b03c1acf --- /dev/null +++ b/ui/tests/integration/components/app-item/build-test.ts @@ -0,0 +1,91 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { getUnixTime, subMinutes } from 'date-fns'; +import { a11yAudit } from 'ember-a11y-testing/test-support'; +import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; + +module('Integration | Component | app-item/build', function (hooks) { + setupRenderingTest(hooks); + + test('with a build and a push', async function (assert) { + this.set('build', { + sequence: 3, + status: { + state: 2, + startTime: minutesAgo(3), + completeTime: minutesAgo(2), + }, + component: { + type: 1, + name: 'docker', + }, + pushedArtifact: { + component: { + type: 2, + name: 'docker', + }, + status: { + state: 2, + startTime: minutesAgo(2), + completeTime: minutesAgo(1), + }, + }, + }); + + await render(hbs` +
      + +
    + `); + await a11yAudit(); + + assert.dom('[data-test-app-item-build]').includesText('v3'); + assert.dom('[data-test-icon-type="logo-docker-color"]').exists(); + assert.dom('[data-test-app-item-build]').includesText('Pushed to Docker'); + assert.dom('[data-test-operation-status-indicator="success"]').exists(); + assert.dom('[data-test-app-item-build]').includesText('1 minute ago'); + assert.dom('[data-test-app-item-build]').includesText('Built in 1 minute'); + }); + + test('with no push', async function (assert) { + this.set('build', { + sequence: 3, + status: { + state: 2, + startTime: minutesAgo(3), + completeTime: minutesAgo(2), + }, + component: { + type: 1, + name: 'docker', + }, + pushedArtifact: null, + }); + + await render(hbs` +
      + +
    + `); + await a11yAudit(); + + assert.dom('[data-test-app-item-build]').includesText('v3'); + assert.dom('[data-test-icon-type="logo-docker-color"]').exists(); + assert.dom('[data-test-app-item-build]').includesText('Built with Docker'); + assert.dom('[data-test-operation-status-indicator="success"]').exists(); + assert.dom('[data-test-app-item-build]').includesText('2 minutes ago'); + }); +}); + +function minutesAgo(n: number): Timestamp.AsObject { + let now = new Date(); + let date = subMinutes(now, n); + let result = { + seconds: getUnixTime(date), + nanos: 0, + }; + + return result; +} diff --git a/ui/translations/en-us.yaml b/ui/translations/en-us.yaml index d7ea44f6e3d..5d3c6ed1785 100644 --- a/ui/translations/en-us.yaml +++ b/ui/translations/en-us.yaml @@ -148,3 +148,18 @@ status_report_indicator: checking_now: Checking now… last_checked: Last checked unknown: Status unknown + +app_item_build: + built_in: Built in {duration} + +build_status: + type-1: # BUILDER + state-0: Building with # UNKNOWN + state-1: Building with # RUNNING + state-2: Built with # SUCCESS + state-3: Failed to build with # ERROR + type-2: # REGISTRY + state-0: Pushing to # UNKNOWN + state-1: Pushing to # RUNNING + state-2: Pushed to # SUCCESS + state-3: Failed to push to # ERROR