diff --git a/src/__fixtures__/libraries.js b/src/__fixtures__/libraries.js index ecc4635d..32db8e49 100644 --- a/src/__fixtures__/libraries.js +++ b/src/__fixtures__/libraries.js @@ -1,33 +1,24 @@ -import { config } from '~/src/config/config.js' - -const githubOrg = config.get('githubOrg') - -// TODO update fixture once we have libraries // Response from portalBackendApi/libraries const librariesFixture = { message: 'success', - libraries: [ - { - id: 'cdp-node-frontend-library', - description: 'Core delivery platform npm library', - primaryLanguage: 'JavaScript', - url: `https://github.com/${githubOrg}/cdp-npm-library`, - isArchived: false, - isLibrary: true, - isPrivate: true, - createdAt: '2023-04-26T15:27:09+00:00', - teams: ['cdp-platform'] - }, + repositories: [ { - id: 'cdp-node-auth-library', - description: 'Core delivery platform auth library', + id: 'hapi-tracing', + description: 'Git repository for hapi-tracing', primaryLanguage: 'JavaScript', - url: `https://github.com/${githubOrg}/cdp-auth-library`, + url: 'https://github.com/DEFRA/hapi-tracing', isArchived: false, - isLibrary: true, - isPrivate: true, - createdAt: '2022-04-26T15:27:10+00:00', - teams: ['cdp-platform'] + isTemplate: false, + isPrivate: false, + createdAt: '2024-12-09T13:08:51+00:00', + teams: [ + { + github: 'cdp-platform', + teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', + name: 'Platform' + } + ], + topics: ['cdp', 'repository', 'library'] } ] } diff --git a/src/__fixtures__/library.js b/src/__fixtures__/library.js index 558b11ff..e4fce731 100644 --- a/src/__fixtures__/library.js +++ b/src/__fixtures__/library.js @@ -1,21 +1,23 @@ -import { config } from '~/src/config/config.js' - -const githubOrg = config.get('githubOrg') - -// TODO update fixture once we have libraries -// Response from portalBackendApi/libraries/cdp-node-frontend-library +// Response from portalBackendApi/libraries/hapi-tracing const libraryFixture = { message: 'success', - library: { - id: 'cdp-node-frontend-library', - description: 'Core delivery platform npm library', + repository: { + id: 'hapi-tracing', + description: 'Git repository for hapi-tracing', primaryLanguage: 'JavaScript', - url: `https://github.com/${githubOrg}/cdp-npm-library`, + url: 'https://github.com/DEFRA/hapi-tracing', isArchived: false, - isLibrary: true, - isPrivate: true, - createdAt: '2023-04-26T15:27:09+00:00', - teams: ['cdp-platform'] + isTemplate: false, + isPrivate: false, + createdAt: '2024-12-09T13:08:51+00:00', + teams: [ + { + github: 'cdp-platform', + teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', + name: 'Platform' + } + ], + topics: ['cdp', 'repository', 'library'] } } diff --git a/src/__fixtures__/template.js b/src/__fixtures__/template.js index 1b215dd4..a2025de0 100644 --- a/src/__fixtures__/template.js +++ b/src/__fixtures__/template.js @@ -1,26 +1,24 @@ -import { config } from '~/src/config/config.js' - -const githubOrg = config.get('githubOrg') - -// Response from portalBackendApi/templates/cdp-portal-frontend +// Response from portalBackendApi/templates/cdp-dotnet-backend-template const templateFixture = { message: 'success', - template: { - id: 'cdp-portal-frontend', - description: 'The Core Delivery Platform Portal.', - primaryLanguage: 'JavaScript', - url: `https://github.com/${githubOrg}/cdp-portal-frontend`, + repository: { + id: 'cdp-dotnet-backend-template', + description: + 'C# ASP.NET Minimial API template with MongoDB, FluentValidation, Swagger and Serilog logging', + primaryLanguage: 'C#', + url: 'https://github.com/DEFRA/cdp-dotnet-backend-template', isArchived: false, - isTemplate: false, - isPrivate: true, - createdAt: '2023-04-12T17:16:48+00:00', + isTemplate: true, + isPrivate: false, + createdAt: '2023-08-24T07:08:56+00:00', teams: [ { github: 'cdp-platform', teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', name: 'Platform' } - ] + ], + topics: ['backend', 'cdp', 'dotnet', 'template'] } } diff --git a/src/__fixtures__/templates.js b/src/__fixtures__/templates.js index b082ea49..b93479bf 100644 --- a/src/__fixtures__/templates.js +++ b/src/__fixtures__/templates.js @@ -1,16 +1,49 @@ -import { config } from '~/src/config/config.js' - -const githubOrg = config.get('githubOrg') - // Response from portalBackendApi/templates const templatesFixture = { message: 'success', - templates: [ + repositories: [ + { + id: 'cdp-dotnet-backend-template', + description: + 'C# ASP.NET Minimial API template with MongoDB, FluentValidation, Swagger and Serilog logging', + primaryLanguage: 'C#', + url: 'https://github.com/DEFRA/cdp-dotnet-backend-template', + isArchived: false, + isTemplate: true, + isPrivate: false, + createdAt: '2023-08-24T07:08:56+00:00', + teams: [ + { + github: 'cdp-platform', + teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', + name: 'Platform' + } + ], + topics: ['backend', 'cdp', 'dotnet', 'template'] + }, + { + id: 'cdp-node-backend-template', + description: 'Core delivery platform Node.js Backend Template', + primaryLanguage: 'JavaScript', + url: 'https://github.com/DEFRA/cdp-node-backend-template', + isArchived: false, + isTemplate: true, + isPrivate: false, + createdAt: '2023-06-20T12:10:50+00:00', + teams: [ + { + github: 'cdp-platform', + teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', + name: 'Platform' + } + ], + topics: ['cdp', 'template', 'backend', 'node'] + }, { id: 'cdp-node-frontend-template', description: 'Core delivery platform Node.js Frontend Template', primaryLanguage: 'JavaScript', - url: `https://github.com/${githubOrg}/cdp-node-frontend-template`, + url: 'https://github.com/DEFRA/cdp-node-frontend-template', isArchived: false, isTemplate: true, isPrivate: false, @@ -21,42 +54,44 @@ const templatesFixture = { teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', name: 'Platform' } - ] + ], + topics: ['cdp', 'frontend', 'node', 'template'] }, { - id: 'cdp-node-backend-template', - description: 'Core delivery platform Node.js Backend Template', + id: 'cdp-node-journey-test-suite-template', + description: 'Git repository for cdp-node-env-test-suite-template', primaryLanguage: 'JavaScript', - url: `https://github.com/${githubOrg}/cdp-node-backend-template`, + url: 'https://github.com/DEFRA/cdp-node-journey-test-suite-template', isArchived: false, isTemplate: true, isPrivate: false, - createdAt: '2023-06-20T12:10:50+00:00', + createdAt: '2024-01-24T14:49:52+00:00', teams: [ { github: 'cdp-platform', teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', name: 'Platform' } - ] + ], + topics: ['cdp', 'template', 'test-suite', 'journey'] }, { - id: 'cdp-dotnet-backend-template', - description: - 'C# ASP.NET Minimial API template with MongoDB, FluentValidation, Swagger and Serilog logging', - primaryLanguage: 'C#', - url: `https://github.com/${githubOrg}/cdp-dotnet-backend-template`, + id: 'cdp-perf-test-suite-template', + description: 'Git repository for cdp-perf-test-suite-template', + primaryLanguage: 'Shell', + url: 'https://github.com/DEFRA/cdp-perf-test-suite-template', isArchived: false, isTemplate: true, isPrivate: false, - createdAt: '2023-08-24T07:08:56+00:00', + createdAt: '2024-04-17T12:10:51+00:00', teams: [ { github: 'cdp-platform', teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474', name: 'Platform' } - ] + ], + topics: ['cdp', 'repository', 'template', 'test-suite', 'performance'] } ] } diff --git a/src/server/admin/features/controllers/features-list.js b/src/server/admin/features/controllers/features-list.js index f6d3ffbf..8fefaa35 100644 --- a/src/server/admin/features/controllers/features-list.js +++ b/src/server/admin/features/controllers/features-list.js @@ -4,10 +4,20 @@ import { transformFeaturesToEntityRows } from '~/src/server/admin/features/trans const listFeaturesController = { handler: async (request, h) => { const featureToggles = await findAllFeatureToggles(request) - const entityRows = transformFeaturesToEntityRows(featureToggles) + const rows = transformFeaturesToEntityRows(featureToggles) + return h.view('admin/features/views/features-list', { pageTitle: 'Admin - Feature Toggles', - entityRows + tableData: { + headers: [ + { id: 'feature', text: 'Feature', width: '15' }, + { id: 'status', text: 'Status', width: '10' }, + { id: 'activated', text: 'Activated', width: '10' }, + { id: 'actions', text: 'Actions', width: '20' } + ], + rows, + noResult: 'No feature flags found' + } }) } } diff --git a/src/server/admin/features/transformers/transform-feature-to-entity-row.js b/src/server/admin/features/transformers/transform-feature-to-entity-row.js index a4ec9c7f..1c0c0cef 100644 --- a/src/server/admin/features/transformers/transform-feature-to-entity-row.js +++ b/src/server/admin/features/transformers/transform-feature-to-entity-row.js @@ -8,41 +8,58 @@ function transformFeaturesToEntityRows(features) { } function transformFeatureToEntityRow(feature) { - return [ - { - kind: 'text', - value: feature.title - }, - { - kind: 'text', - value: feature.enabled ? 'Active' : 'Not active' - }, - ...(feature.enabled - ? [ - { - kind: 'date', - value: feature.created - }, - { - kind: 'button', - value: 'Deactivate', - url: feature.urlPrefix + '/delete', - classes: 'app-button--small app-button--destructive' - } - ] - : [ - { - kind: 'text', - value: noValue - }, - { - kind: 'button', - value: 'Activate', - url: feature.urlPrefix, - classes: 'app-button--small' - } - ]) - ] + return { + cells: [ + { + headers: 'feature', + entity: { kind: 'text', value: feature.title } + }, + { + headers: 'status', + entity: { + kind: 'text', + value: feature.enabled ? 'Active' : 'Not active' + } + }, + ...(feature.enabled + ? [ + { + headers: 'activated', + entity: { + kind: 'date', + value: feature.created + } + }, + { + headers: 'actions', + entity: { + kind: 'button', + value: 'Deactivate', + url: feature.urlPrefix + '/delete', + classes: 'app-button--small app-button--destructive' + } + } + ] + : [ + { + headers: 'activated', + entity: { + kind: 'text', + value: noValue + } + }, + { + headers: 'actions', + entity: { + kind: 'button', + value: 'Activate', + url: feature.urlPrefix, + classes: 'app-button--small' + } + } + ]) + ] + } } export { transformFeaturesToEntityRows, transformFeatureToEntityRow } diff --git a/src/server/admin/features/transformers/transform-feature-to-entity-row.test.js b/src/server/admin/features/transformers/transform-feature-to-entity-row.test.js index debf2d0d..2a2b4455 100644 --- a/src/server/admin/features/transformers/transform-feature-to-entity-row.test.js +++ b/src/server/admin/features/transformers/transform-feature-to-entity-row.test.js @@ -2,7 +2,6 @@ import { transformFeaturesToEntityRows, transformFeatureToEntityRow } from '~/src/server/admin/features/transformers/transform-feature-to-entity-row.js' -import { noValue } from '~/src/server/common/constants/no-value.js' describe('transformFeatureToEntityRow', () => { test('Should return entity row for active feature', () => { @@ -15,27 +14,40 @@ describe('transformFeatureToEntityRow', () => { const result = transformFeatureToEntityRow(feature) - expect(result).toHaveLength(4) - expect(result).toEqual([ - { - kind: 'text', - value: 'Feature 1' - }, - { - kind: 'text', - value: 'Active' - }, - { - kind: 'date', - value: '2021-01-01' - }, - { - kind: 'button', - value: 'Deactivate', - url: '/some-url/delete', - classes: 'app-button--small app-button--destructive' - } - ]) + expect(result).toEqual({ + cells: [ + { + entity: { + kind: 'text', + value: 'Feature 1' + }, + headers: 'feature' + }, + { + entity: { + kind: 'text', + value: 'Active' + }, + headers: 'status' + }, + { + entity: { + kind: 'date', + value: '2021-01-01' + }, + headers: 'activated' + }, + { + entity: { + classes: 'app-button--small app-button--destructive', + kind: 'button', + url: '/some-url/delete', + value: 'Deactivate' + }, + headers: 'actions' + } + ] + }) }) test('Should return entity row for inactive feature', () => { @@ -47,27 +59,40 @@ describe('transformFeatureToEntityRow', () => { const result = transformFeatureToEntityRow(feature) - expect(result).toHaveLength(4) - expect(result).toEqual([ - { - kind: 'text', - value: 'Feature 2' - }, - { - kind: 'text', - value: 'Not active' - }, - { - kind: 'text', - value: noValue - }, - { - kind: 'button', - value: 'Activate', - url: '/some-url', - classes: 'app-button--small' - } - ]) + expect(result).toEqual({ + cells: [ + { + entity: { + kind: 'text', + value: 'Feature 2' + }, + headers: 'feature' + }, + { + entity: { + kind: 'text', + value: 'Not active' + }, + headers: 'status' + }, + { + entity: { + kind: 'text', + value: '- - -' + }, + headers: 'activated' + }, + { + entity: { + classes: 'app-button--small', + kind: 'button', + url: '/some-url', + value: 'Activate' + }, + headers: 'actions' + } + ] + }) }) }) @@ -89,12 +114,76 @@ describe('transformFeaturesToEntityRows', () => { const result = transformFeaturesToEntityRows(features) - expect(result).toHaveLength(2) - - expect(result[0][0].value).toBe('Feature 1') - expect(result[0][1].value).toBe('Active') - expect(result[1][0].value).toBe('Feature 2') - expect(result[1][1].value).toBe('Not active') + expect(result).toEqual([ + { + cells: [ + { + entity: { + kind: 'text', + value: 'Feature 1' + }, + headers: 'feature' + }, + { + entity: { + kind: 'text', + value: 'Active' + }, + headers: 'status' + }, + { + entity: { + kind: 'date', + value: '2021-01-01' + }, + headers: 'activated' + }, + { + entity: { + classes: 'app-button--small app-button--destructive', + kind: 'button', + url: '/some-url/delete', + value: 'Deactivate' + }, + headers: 'actions' + } + ] + }, + { + cells: [ + { + entity: { + kind: 'text', + value: 'Feature 2' + }, + headers: 'feature' + }, + { + entity: { + kind: 'text', + value: 'Not active' + }, + headers: 'status' + }, + { + entity: { + kind: 'text', + value: '- - -' + }, + headers: 'activated' + }, + { + entity: { + classes: 'app-button--small', + kind: 'button', + url: '/some-url', + value: 'Activate' + }, + headers: 'actions' + } + ] + } + ]) }) test('Should return empty array when no features', () => { diff --git a/src/server/admin/features/views/features-list.njk b/src/server/admin/features/views/features-list.njk index 24fd14e1..c0d16573 100644 --- a/src/server/admin/features/views/features-list.njk +++ b/src/server/admin/features/views/features-list.njk @@ -4,19 +4,11 @@ {% call appSplitPane() %} {{ appPageHeading({ - text: "Feature Toggles" + text: "Feature Toggles", + intro: "Portal wide feature toggles, to toggle features on/off" }) }} - {{ appEntityList({ - headings: [ - { text: "Feature", size: "large" }, - { text: "Status", size: "medium" }, - { text: "Activated", size: "medium" }, - { text: "Actions", size: "medium" } - ], - entityRows: entityRows, - noResult: noResult - }) }} + {{ appEntityTable(tableData) }} {% endcall %} {% endblock %} diff --git a/src/server/admin/permissions/controllers/permissions-list.js b/src/server/admin/permissions/controllers/permissions-list.js index b8a15acd..24e965b8 100644 --- a/src/server/admin/permissions/controllers/permissions-list.js +++ b/src/server/admin/permissions/controllers/permissions-list.js @@ -3,22 +3,36 @@ import { fetchScopes } from '~/src/server/admin/permissions/helpers/fetchers.js' const permissionsListController = { handler: async (request, h) => { const { scopes } = await fetchScopes(request) - const entityRows = scopes.map((scope) => [ - { - kind: 'link', - url: `/admin/permissions/${scope.scopeId}`, - value: scope.value - }, - { - kind: 'text', - value: scope.description - } - ]) + const rows = scopes.map((scope) => ({ + cells: [ + { + headers: 'value', + entity: { + kind: 'link', + url: `/admin/permissions/${scope.scopeId}`, + value: scope.value + } + }, + { + headers: 'description', + entity: { + kind: 'text', + value: scope.description + } + } + ] + })) return h.view('admin/permissions/views/permissions-list', { pageTitle: 'Permissions', - heading: 'Permissions', - entityRows + tableData: { + headers: [ + { id: 'value', text: 'Value', width: '20' }, + { id: 'description', text: 'Description', width: '80' } + ], + rows, + noResult: 'No permissions found' + } }) } } diff --git a/src/server/admin/permissions/views/permissions-list.njk b/src/server/admin/permissions/views/permissions-list.njk index 35465658..00683aa7 100644 --- a/src/server/admin/permissions/views/permissions-list.njk +++ b/src/server/admin/permissions/views/permissions-list.njk @@ -5,22 +5,15 @@ {{ appPageHeading({ text: "Permissions", + intro: "Portal wide permissions details, create, view and assign permissions to users and teams", cta: { text: "Create new permission", href: routeLookup('admin/permission/create') } }) }} - {{ appEntityList({ - headings: [ - { text: "Value", size: "medium" }, - { text: "Description", size: "gargantuan" } - ], - entityRows: entityRows, - noResult: "Currently there are no permissions available" - }) }} + {{ appEntityTable(tableData) }} {% endcall %} - {% endblock %} diff --git a/src/server/admin/teams/controllers/teams-list.js b/src/server/admin/teams/controllers/teams-list.js index 833ec2ef..e41a35d7 100644 --- a/src/server/admin/teams/controllers/teams-list.js +++ b/src/server/admin/teams/controllers/teams-list.js @@ -4,12 +4,24 @@ import { transformTeamToEntityRow } from '~/src/server/admin/teams/transformers/ const teamsListController = { handler: async (request, h) => { const { teams } = await fetchCdpTeams() - const entityRows = teams?.map(transformTeamToEntityRow) + const rows = teams?.map(transformTeamToEntityRow) return h.view('admin/teams/views/teams-list', { pageTitle: 'Teams', - entityRows, - noResult: 'Currently there are no teams' + tableData: { + headers: [ + { id: 'name', text: 'Name', width: '15' }, + { id: 'description', text: 'Description', width: '15' }, + { id: 'github-team', text: 'GitHub team', width: '15' }, + { id: 'service-codes', text: 'Service Codes', width: '5' }, + { id: 'alert-emails', text: 'Alert Emails', width: '20' }, + { id: 'members', text: 'Members', width: '10' }, + { id: 'last-updated', text: 'Last Updated', width: '10' }, + { id: 'created', text: 'Created', width: '10' } + ], + rows, + noResult: 'No teams found' + } }) } } diff --git a/src/server/admin/teams/transformers/transform-team-to-entity-row.js b/src/server/admin/teams/transformers/transform-team-to-entity-row.js index c090789e..c2bb0d38 100644 --- a/src/server/admin/teams/transformers/transform-team-to-entity-row.js +++ b/src/server/admin/teams/transformers/transform-team-to-entity-row.js @@ -3,35 +3,63 @@ import { config } from '~/src/config/config.js' function transformTeamToEntityRow(team) { const githubOrg = config.get('githubOrg') - return [ - { - kind: 'link', - value: team.name, - url: `/admin/teams/${team.teamId}` - }, - { - kind: 'text', - value: team.description - }, - { - kind: 'link', - value: team.github ? `@${team.github}` : null, - url: `https://github.com/orgs/${githubOrg}/teams/${team.github}`, - newWindow: true - }, - { - kind: 'text', - value: team.serviceCodes - }, - { - kind: 'html', - value: team.alertEmailAddresses?.join('
') - }, - { - kind: 'text', - value: team.users.length - } - ] + return { + cells: [ + { + headers: 'name', + entity: { + kind: 'link', + value: team.name, + url: `/admin/teams/${team.teamId}` + } + }, + { + headers: 'description', + entity: { + kind: 'text', + value: team.description + } + }, + { + headers: 'github-team', + entity: { + kind: 'link', + value: team.github ? `@${team.github}` : null, + url: `https://github.com/orgs/${githubOrg}/teams/${team.github}`, + newWindow: true + } + }, + { + headers: 'service-codes', + entity: { + kind: 'text', + value: team.serviceCodes + } + }, + { + headers: 'alert-emails', + entity: { + kind: 'html', + value: team.alertEmailAddresses?.join('
') + } + }, + { + headers: 'members', + entity: { + kind: 'text', + value: team.users.length + } + }, + { + headers: 'last-updated', + entity: { kind: 'date', value: team.updatedAt } + }, + { + headers: 'created', + entity: { kind: 'date', value: team.createdAt } + } + ] + } } export { transformTeamToEntityRow } diff --git a/src/server/admin/teams/transformers/transform-team-to-entity-row.test.js b/src/server/admin/teams/transformers/transform-team-to-entity-row.test.js index 0c0d82c2..abe4e66d 100644 --- a/src/server/admin/teams/transformers/transform-team-to-entity-row.test.js +++ b/src/server/admin/teams/transformers/transform-team-to-entity-row.test.js @@ -1,39 +1,70 @@ -import { config } from '~/src/config/config.js' import { cdpTeamFixture } from '~/src/__fixtures__/admin/cdp-team.js' import { transformTeamToEntityRow } from '~/src/server/admin/teams/transformers/transform-team-to-entity-row.js' -const githubOrg = config.get('githubOrg') - describe('#transformCdpTeamToEntityRow', () => { test('Should provide expected team row transformation', () => { - expect(transformTeamToEntityRow(cdpTeamFixture.team)).toEqual([ - { - kind: 'link', - url: '/admin/teams/aabe63e7-87ef-4beb-a596-c810631fc474', - value: 'Platform' - }, - { - kind: 'text', - value: 'The team that runs the platform' - }, - { - kind: 'link', - newWindow: true, - url: `https://github.com/orgs/${githubOrg}/teams/cdp-platform`, - value: '@cdp-platform' - }, - { - kind: 'text', - value: ['CDP'] - }, - { - kind: 'html', - value: 'alerts@cdp.com' - }, - { - kind: 'text', - value: 2 - } - ]) + expect(transformTeamToEntityRow(cdpTeamFixture.team)).toEqual({ + cells: [ + { + entity: { + kind: 'link', + url: '/admin/teams/aabe63e7-87ef-4beb-a596-c810631fc474', + value: 'Platform' + }, + headers: 'name' + }, + { + entity: { + kind: 'text', + value: 'The team that runs the platform' + }, + headers: 'description' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/orgs/DEFRA/teams/cdp-platform', + value: '@cdp-platform' + }, + headers: 'github-team' + }, + { + entity: { + kind: 'text', + value: ['CDP'] + }, + headers: 'service-codes' + }, + { + entity: { + kind: 'html', + value: 'alerts@cdp.com' + }, + headers: 'alert-emails' + }, + { + entity: { + kind: 'text', + value: 2 + }, + headers: 'members' + }, + { + entity: { + kind: 'date', + value: '2023-10-03T11:11:31.085Z' + }, + headers: 'last-updated' + }, + { + entity: { + kind: 'date', + value: '2023-09-28T12:52:14.673Z' + }, + headers: 'created' + } + ] + }) }) }) diff --git a/src/server/admin/teams/views/teams-list.njk b/src/server/admin/teams/views/teams-list.njk index a56c7390..4f6deeb9 100644 --- a/src/server/admin/teams/views/teams-list.njk +++ b/src/server/admin/teams/views/teams-list.njk @@ -5,26 +5,15 @@ {{ appPageHeading({ text: "Teams", + intro: "Portal teams details, create, edit, view and add/remove users or permissions to teams", cta: { text: "Create new team", href: routeLookup('admin/teams/create') } }) }} - {{ appEntityList({ - headings: [ - { text: "Name", size: "medium" }, - { text: "Description", size: "massive" }, - { text: "GitHub team", size: "medium" }, - { text: "Service Codes", size: "small" }, - { text: "Alert Emails", size: "large" }, - { text: "Members", size: "small" } - ], - entityRows: entityRows, - noResult: noResult - }) }} + {{ appEntityTable(tableData) }} {% endcall %} - {% endblock %} diff --git a/src/server/admin/users/controllers/users-list.js b/src/server/admin/users/controllers/users-list.js index 5c7a02cc..19ee21dd 100644 --- a/src/server/admin/users/controllers/users-list.js +++ b/src/server/admin/users/controllers/users-list.js @@ -4,13 +4,21 @@ import { transformUserToEntityRow } from '~/src/server/admin/users/transformers/ const usersListController = { handler: async (request, h) => { const { users } = await fetchCdpUsers() - - const entityRows = users?.map(transformUserToEntityRow) + const rows = users?.map(transformUserToEntityRow) return h.view('admin/users/views/users-list', { pageTitle: 'Users', - entityRows, - noResult: 'Currently there are no users' + tableData: { + headers: [ + { id: 'name', text: 'Name', width: '15' }, + { id: 'email', text: 'Email', width: '15' }, + { id: 'github-user', text: 'GitHub user', width: '15' }, + { id: 'last-updated', text: 'Last Updated', width: '10' }, + { id: 'created', text: 'Created', width: '10' } + ], + rows, + noResult: 'No users found' + } }) } } diff --git a/src/server/admin/users/transformers/transform-user-to-entity-row.js b/src/server/admin/users/transformers/transform-user-to-entity-row.js index 84983b1e..81e5bb91 100644 --- a/src/server/admin/users/transformers/transform-user-to-entity-row.js +++ b/src/server/admin/users/transformers/transform-user-to-entity-row.js @@ -3,32 +3,43 @@ import { config } from '~/src/config/config.js' function transformUserToEntityRow(user) { const githubOrg = config.get('githubOrg') - return [ - { - kind: 'link', - value: user.name ? user.name : null, - url: `/admin/users/${user.userId}` - }, - { - kind: 'link', - value: user.email, - url: `mailto:${user.email}` - }, - { - kind: 'link', - value: user.github ? `@${user.github}` : null, - url: `https://github.com/orgs/${githubOrg}/people/${user.github}`, - newWindow: true - }, - { - kind: 'text', - value: user.defraAwsId - }, - { - kind: 'text', - value: user.defraVpnId - } - ] + return { + cells: [ + { + headers: 'name', + entity: { + kind: 'link', + value: user.name ? user.name : null, + url: `/admin/users/${user.userId}` + } + }, + { + headers: 'email', + entity: { + kind: 'link', + value: user.email, + url: `mailto:${user.email}` + } + }, + { + headers: 'github-user', + entity: { + kind: 'link', + value: user.github ? `@${user.github}` : null, + url: `https://github.com/orgs/${githubOrg}/people/${user.github}`, + newWindow: true + } + }, + { + headers: 'last-updated', + entity: { kind: 'date', value: user.updatedAt } + }, + { + headers: 'created', + entity: { kind: 'date', value: user.createdAt } + } + ] + } } export { transformUserToEntityRow } diff --git a/src/server/admin/users/transformers/transform-user-to-entity-row.test.js b/src/server/admin/users/transformers/transform-user-to-entity-row.test.js index 38fab12e..a1d7eeb4 100644 --- a/src/server/admin/users/transformers/transform-user-to-entity-row.test.js +++ b/src/server/admin/users/transformers/transform-user-to-entity-row.test.js @@ -1,36 +1,50 @@ -import { config } from '~/src/config/config.js' import { cdpUserFixture } from '~/src/__fixtures__/admin/cdp-user.js' import { transformUserToEntityRow } from '~/src/server/admin/users/transformers/transform-user-to-entity-row.js' -const githubOrg = config.get('githubOrg') - describe('#transformUserToEntityRow', () => { test('Should provide expected user entity row transformation', () => { - expect(transformUserToEntityRow(cdpUserFixture.user)).toEqual([ - { - kind: 'link', - url: '/admin/users/1398fa86-98a2-4ee8-84bb-2468cc71d0ec', - value: 'B. A. Baracus' - }, - { - kind: 'link', - url: 'mailto:B.A.Baracus@defradev.onmicrosoft.com', - value: 'B.A.Baracus@defradev.onmicrosoft.com' - }, - { - kind: 'link', - newWindow: true, - url: `https://github.com/orgs/${githubOrg}/people/BABaracus`, - value: '@BABaracus' - }, - { - kind: 'text', - value: 'FGHyu-232342-234234' - }, - { - kind: 'text', - value: '345345-345345' - } - ]) + expect(transformUserToEntityRow(cdpUserFixture.user)).toEqual({ + cells: [ + { + entity: { + kind: 'link', + url: '/admin/users/1398fa86-98a2-4ee8-84bb-2468cc71d0ec', + value: 'B. A. Baracus' + }, + headers: 'name' + }, + { + entity: { + kind: 'link', + url: 'mailto:B.A.Baracus@defradev.onmicrosoft.com', + value: 'B.A.Baracus@defradev.onmicrosoft.com' + }, + headers: 'email' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/orgs/DEFRA/people/BABaracus', + value: '@BABaracus' + }, + headers: 'github-user' + }, + { + entity: { + kind: 'date', + value: '2023-08-24T15:31:52.259Z' + }, + headers: 'last-updated' + }, + { + entity: { + kind: 'date', + value: '2023-08-23T16:17:57.883Z' + }, + headers: 'created' + } + ] + }) }) }) diff --git a/src/server/admin/users/views/users-list.njk b/src/server/admin/users/views/users-list.njk index 47f20f13..d02a2d84 100644 --- a/src/server/admin/users/views/users-list.njk +++ b/src/server/admin/users/views/users-list.njk @@ -1,31 +1,19 @@ {% extends "layouts/page.njk" %} {% block content %} - {% call appSplitPane() %} {{ appPageHeading({ text: "Users", + intro: "Portal users details, create, edit, view or delete users", cta: { text: "Create new user", href: routeLookup('admin/users/create') } }) }} - {{ appEntityList({ - headings: [ - { text: "Name", size: "large" }, - { text: "Email", size: "massive" }, - { text: "GitHub user", size: "medium" }, - { text: "Defra AWS ID", size: "medium" }, - { text: "Defra VPN ID", size: "medium" } - ], - entityRows: entityRows, - noResult: noResult - }) }} - + {{ appEntityTable(tableData) }} {% endcall %} - {% endblock %} diff --git a/src/server/common/components/entity-table/_entity-table.scss b/src/server/common/components/entity-table/_entity-table.scss index 10badf32..73d2567b 100644 --- a/src/server/common/components/entity-table/_entity-table.scss +++ b/src/server/common/components/entity-table/_entity-table.scss @@ -10,7 +10,6 @@ border-spacing: 0; padding: 0; width: 100%; - margin-top: -8px; text-align: left; empty-cells: show; @@ -53,6 +52,11 @@ } } +.app-entity-table__cell--centered { + text-align: center; + vertical-align: middle; +} + .app-entity-table__cell--slim { padding: 0; } @@ -61,6 +65,7 @@ color: govuk-colour("dark-grey"); } -.app-entity-table__cell--20-fixed { +.app-entity-table__cell--owned { width: 20px; + padding: 0 govuk-spacing(2); } diff --git a/src/server/common/components/entity-table/template.njk b/src/server/common/components/entity-table/template.njk index 50e4cfcf..0104141a 100644 --- a/src/server/common/components/entity-table/template.njk +++ b/src/server/common/components/entity-table/template.njk @@ -8,10 +8,15 @@ {% for header in params.headers %} - {{ header.text | title }} + {% if header.text %} + {{ header.text | title }} + {% else %} + {{ header.id | title }} + {% endif %} {% endfor %} @@ -21,8 +26,12 @@ {% for cell in row.cells %} - {% set cellClasses = ["app-entity-table__cell", cell.classes if cell.classes, - "app-entity-table__cell--slim" if cell.isSlim] | join(" ") | trim %} + {% set cellClasses = [ + "app-entity-table__cell", + cell.classes if cell.classes, + "app-entity-table__cell--slim" if cell.isSlim, + "app-entity-table__cell--centered" if cell.isCentered + ] | join(" ") | trim %} diff --git a/src/server/common/components/entity/_entity.scss b/src/server/common/components/entity/_entity.scss index 61b73706..803739f1 100644 --- a/src/server/common/components/entity/_entity.scss +++ b/src/server/common/components/entity/_entity.scss @@ -22,6 +22,11 @@ @extend %govuk-list--bullet; } +.app-entity__group { + display: flex; + align-items: center; +} + .app-entity__list-item { @include govuk-text-colour; margin: 0; diff --git a/src/server/common/components/entity/template.njk b/src/server/common/components/entity/template.njk index 4fc4a64c..63c2f1ec 100644 --- a/src/server/common/components/entity/template.njk +++ b/src/server/common/components/entity/template.njk @@ -77,11 +77,13 @@ data-testid="app-entity{% if params.index %}-{{ params.index }}{% endif %}"> {% elseif params.kind === "group" %} - {% for entity in params.value %} - {{ appEntity(entity) }} - {% else %} - {{ noValue }} - {% endfor %} + + {% for entity in params.value %} + {{ appEntity(entity) }} + {% else %} + {{ noValue }} + {% endfor %} + {% endif %} diff --git a/src/server/common/components/filters/_filters.scss b/src/server/common/components/filters/_filters.scss index 32f10594..2f67af2f 100644 --- a/src/server/common/components/filters/_filters.scss +++ b/src/server/common/components/filters/_filters.scss @@ -16,7 +16,7 @@ .app-filters-form { padding: govuk-spacing(4) govuk-spacing(2); - margin: govuk-spacing(4) govuk-spacing(-2) govuk-spacing(2); + margin: govuk-spacing(4) govuk-spacing(-2) 0; background-color: $app-light-grey; border-radius: $app-border-radius; border-top: 1px solid $app-mid-grey; diff --git a/src/server/common/components/page-heading/_page-heading.scss b/src/server/common/components/page-heading/_page-heading.scss index 8517a670..9168ea49 100644 --- a/src/server/common/components/page-heading/_page-heading.scss +++ b/src/server/common/components/page-heading/_page-heading.scss @@ -9,6 +9,10 @@ margin: 0; } +.app-page-heading--intro { + margin: govuk-spacing(2) 0 govuk-spacing(4); +} + .app-page-heading--cta { margin: govuk-spacing(3) 0 govuk-spacing(6); } diff --git a/src/server/common/components/page-heading/template.njk b/src/server/common/components/page-heading/template.njk index 874eaf35..f23c194f 100644 --- a/src/server/common/components/page-heading/template.njk +++ b/src/server/common/components/page-heading/template.njk @@ -6,6 +6,12 @@

{{ params.text }}

+ {% if params.intro %} +

+ {{ params.intro }} +

+ {% endif %} + {% if params.cta %}

- + Deployed diff --git a/src/server/common/helpers/decorators/repositories.js b/src/server/common/helpers/decorators/repositories.js index fb766f01..a095a9e6 100644 --- a/src/server/common/helpers/decorators/repositories.js +++ b/src/server/common/helpers/decorators/repositories.js @@ -1,7 +1,7 @@ import { repositoryDecorator } from '~/src/server/common/helpers/decorators/repository.js' function repositoriesDecorator(repositories) { - return function addDetail(service) { + return (service) => { let repository if (service.githubUrl) { diff --git a/src/server/common/helpers/sort/sort-by-owner.js b/src/server/common/helpers/sort/sort-by-owner.js index c73f3c78..bca3a9c4 100644 --- a/src/server/common/helpers/sort/sort-by-owner.js +++ b/src/server/common/helpers/sort/sort-by-owner.js @@ -1,12 +1,14 @@ -function sortByOwner(a, b) { - if (a.userOwnsService && !b.userOwnsService) { - return -1 - } - if (!a.userOwnsService && b.userOwnsService) { - return 1 - } +function sortByOwner(prop) { + return (a, b) => { + if (a.isOwner && !b.isOwner) { + return -1 + } + if (!a.isOwner && b.isOwner) { + return 1 + } - return a.serviceName.localeCompare(b.serviceName) + return a[prop].localeCompare(b[prop]) + } } export { sortByOwner } diff --git a/src/server/common/helpers/sort/sort-by-owner.test.js b/src/server/common/helpers/sort/sort-by-owner.test.js index 8ff4400f..abb2a1ab 100644 --- a/src/server/common/helpers/sort/sort-by-owner.test.js +++ b/src/server/common/helpers/sort/sort-by-owner.test.js @@ -1,30 +1,33 @@ import { config } from '~/src/config/config.js' import { servicesFixture } from '~/src/__fixtures__/services/services.js' import { sortByOwner } from '~/src/server/common/helpers/sort/sort-by-owner.js' +import { librariesFixture } from '~/src/__fixtures__/libraries.js' describe('#sortByOwner', () => { const oidcAdminGroupId = config.get('oidcAdminGroupId') const servicesWithOwner = servicesFixture.map((service) => ({ ...service, - userOwnsService: service.teams.some( - (team) => team.teamId === oidcAdminGroupId - ) + isOwner: service.teams.some((team) => team.teamId === oidcAdminGroupId) + })) + const librariesWithOwner = librariesFixture.repositories.map((library) => ({ + ...library, + isOwner: library.teams.some((team) => team.teamId === oidcAdminGroupId) })) describe('With owner information', () => { test('Should sort owned teams first', () => { - expect(servicesWithOwner.sort(sortByOwner)).toEqual([ + expect(servicesWithOwner.sort(sortByOwner('serviceName'))).toEqual([ expect.objectContaining({ serviceName: 'cdp-portal-frontend', - userOwnsService: true + isOwner: true }), expect.objectContaining({ serviceName: 'cdp-user-service-backend', - userOwnsService: true + isOwner: true }), expect.objectContaining({ serviceName: 'forms-designer', - userOwnsService: false + isOwner: false }) ]) }) @@ -32,7 +35,7 @@ describe('#sortByOwner', () => { describe('Without owner information', () => { test('Should sort alphabetically by service name', () => { - expect(servicesFixture.sort(sortByOwner)).toEqual([ + expect(servicesFixture.sort(sortByOwner('serviceName'))).toEqual([ expect.objectContaining({ serviceName: 'cdp-portal-frontend' }), @@ -45,4 +48,15 @@ describe('#sortByOwner', () => { ]) }) }) + + describe('With alternative prop', () => { + test('Should sort owned teams first', () => { + expect(librariesWithOwner.sort(sortByOwner('id'))).toEqual([ + expect.objectContaining({ + id: 'hapi-tracing', + isOwner: true + }) + ]) + }) + }) }) diff --git a/src/server/documentation/views/documentation.njk b/src/server/documentation/views/documentation.njk index c524f3b6..8db04f58 100644 --- a/src/server/documentation/views/documentation.njk +++ b/src/server/documentation/views/documentation.njk @@ -4,13 +4,10 @@ {% block content %} {{ appPageHeading({ - text: "Documentation" + text: "Documentation", + intro: "All things Core Delivery Platform, Architecture, Onboarding, Services, Developer docs and more" }) }} -

- All things Core Delivery Platform, onboarding, test suites, services, and more. -

-
{{ appDocumentation({ nav: nav, content: content, toc: toc }) }} diff --git a/src/server/running-services/controllers/running-services-list.js b/src/server/running-services/controllers/running-services-list.js index eae62b6f..db5a9c55 100644 --- a/src/server/running-services/controllers/running-services-list.js +++ b/src/server/running-services/controllers/running-services-list.js @@ -34,7 +34,7 @@ const runningServicesListController = { tableData: { headers: [ ...(isAuthenticated - ? [{ id: 'owner', text: null, size: '20-fixed' }] + ? [{ id: 'owner', classes: 'app-entity-table__cell--owned' }] : []), { id: 'service', text: 'Service', width: '15' }, { id: 'team', text: 'Team', width: '15' }, diff --git a/src/server/running-services/helpers/build-running-services-table-data.js b/src/server/running-services/helpers/build-running-services-table-data.js index 004c64be..0d1d6dc5 100644 --- a/src/server/running-services/helpers/build-running-services-table-data.js +++ b/src/server/running-services/helpers/build-running-services-table-data.js @@ -79,8 +79,9 @@ async function buildRunningServicesTableData({ pre, query }) { userScopeUUIDs }) + const ownerSorter = sortByOwner('serviceName') const decorator = runningServiceToEntityRow(environments, isAuthenticated) - const rows = services.toSorted(sortByOwner).map(decorator) + const rows = services.toSorted(ownerSorter).map(decorator) return { environments, diff --git a/src/server/running-services/helpers/transformers/running-service-to-entity-row.js b/src/server/running-services/helpers/transformers/running-service-to-entity-row.js index 81203945..05f3ebe3 100644 --- a/src/server/running-services/helpers/transformers/running-service-to-entity-row.js +++ b/src/server/running-services/helpers/transformers/running-service-to-entity-row.js @@ -4,7 +4,7 @@ import { } from '~/src/server/common/helpers/nunjucks/render-component.js' function runningServiceToEntityRow(allEnvironments, isAuthenticated) { - return ({ serviceName, environments, teams, userOwnsService }) => { + return ({ serviceName, environments, teams, isOwner }) => { const serviceTeams = teams .filter((team) => team.teamId) .map((team) => ({ @@ -13,7 +13,7 @@ function runningServiceToEntityRow(allEnvironments, isAuthenticated) { url: `/teams/${team.teamId}` })) - const icon = userOwnsService + const icon = isOwner ? renderComponent( 'tool-tip', { @@ -53,6 +53,8 @@ function runningServiceToEntityRow(allEnvironments, isAuthenticated) { ? [ { headers: 'owner', + isCentered: true, + classes: 'app-entity-table__cell--owned', entity: { kind: 'html', value: icon } } ] diff --git a/src/server/running-services/helpers/transformers/running-service-to-entity-row.test.js b/src/server/running-services/helpers/transformers/running-service-to-entity-row.test.js index 2679114c..165fa755 100644 --- a/src/server/running-services/helpers/transformers/running-service-to-entity-row.test.js +++ b/src/server/running-services/helpers/transformers/running-service-to-entity-row.test.js @@ -37,7 +37,9 @@ describe('#runningServiceToEntityRow', () => { kind: 'html', value: expect.stringContaining('app-star-icon') }, - headers: 'owner' + headers: 'owner', + isCentered: true, + classes: 'app-entity-table__cell--owned' }, { entity: { @@ -168,7 +170,9 @@ describe('#runningServiceToEntityRow', () => { kind: 'html', value: expect.stringContaining('app-star-icon') }, - headers: 'owner' + headers: 'owner', + isCentered: true, + classes: 'app-entity-table__cell--owned' }, { entity: { diff --git a/src/server/running-services/helpers/transformers/running-services.js b/src/server/running-services/helpers/transformers/running-services.js index 0d32b2f2..879ef459 100644 --- a/src/server/running-services/helpers/transformers/running-services.js +++ b/src/server/running-services/helpers/transformers/running-services.js @@ -27,18 +27,18 @@ function transformRunningServices({ deployableService?.teams.filter((team) => team.teamId) ?? [] } - if (!acc[rs.service].userOwnsService) { - acc[rs.service].userOwnsService = acc[rs.service].teams.some((team) => + if (!acc[rs.service].isOwner) { + acc[rs.service].isOwner = acc[rs.service].teams.some((team) => userScopeUUIDs.includes(team.teamId) ) } return acc }, {}) - ).map(([serviceName, { envs, teams, userOwnsService }]) => { + ).map(([serviceName, { envs, teams, isOwner }]) => { return { serviceName, - userOwnsService, + isOwner, environments: envs, teams } diff --git a/src/server/running-services/helpers/transformers/running-services.test.js b/src/server/running-services/helpers/transformers/running-services.test.js index a68c4ab6..138404e9 100644 --- a/src/server/running-services/helpers/transformers/running-services.test.js +++ b/src/server/running-services/helpers/transformers/running-services.test.js @@ -65,7 +65,7 @@ describe('transformRunningServices', () => { teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474' } ], - userOwnsService: true + isOwner: true }, { environments: { @@ -108,7 +108,7 @@ describe('transformRunningServices', () => { }, serviceName: 'cdp-self-service-ops', teams: [], - userOwnsService: false + isOwner: false }, { environments: { @@ -157,7 +157,7 @@ describe('transformRunningServices', () => { teamId: 'aabe63e7-87ef-4beb-a596-c810631fc474' } ], - userOwnsService: true + isOwner: true } ]) }) diff --git a/src/server/running-services/views/list.njk b/src/server/running-services/views/list.njk index baa52451..e1fba6c6 100644 --- a/src/server/running-services/views/list.njk +++ b/src/server/running-services/views/list.njk @@ -4,7 +4,8 @@ {{ appPageHeading({ caption: "Whats running where?", - text: "Running Services" + text: "Running Services", + intro: "All CDP running services, with microservice details and status per environment" }) }} {% call appFilters({ diff --git a/src/server/services/list/controller.js b/src/server/services/list/controller.js index 657f47d1..36abf1cf 100644 --- a/src/server/services/list/controller.js +++ b/src/server/services/list/controller.js @@ -38,7 +38,7 @@ const serviceListController = { tableData: { headers: [ ...(isAuthenticated - ? [{ id: 'owner', text: null, size: '20-fixed' }] + ? [{ id: 'owner', classes: 'app-entity-table__cell--owned' }] : []), { id: 'service', text: 'Service', width: '15' }, { id: 'team', text: 'Team', width: '15' }, diff --git a/src/server/services/list/helpers/build-services-table-data.js b/src/server/services/list/helpers/build-services-table-data.js index ac1a2e2b..c78afbb7 100644 --- a/src/server/services/list/helpers/build-services-table-data.js +++ b/src/server/services/list/helpers/build-services-table-data.js @@ -75,14 +75,15 @@ async function buildServicesTableData({ ) const rowDecorator = serviceToEntityRow(isAuthenticated) + const ownerSorter = sortByOwner('serviceName') const rows = services .map((serviceDetail) => ({ ...serviceDetail, - userOwnsService: serviceDetail.teams.some((team) => + isOwner: serviceDetail.teams.some((team) => userScopeUUIDs.includes(team.teamId) ) })) - .sort(sortByOwner) + .toSorted(ownerSorter) .map(rowDecorator) return { diff --git a/src/server/services/list/transformers/service-to-entity-row.js b/src/server/services/list/transformers/service-to-entity-row.js index f549f92e..0a0d7fec 100644 --- a/src/server/services/list/transformers/service-to-entity-row.js +++ b/src/server/services/list/transformers/service-to-entity-row.js @@ -40,7 +40,7 @@ function serviceToEntityRow(isAuthenticated) { entity: { kind: 'date', value: service.createdAt } } - const icon = service.userOwnsService + const icon = service.isOwner ? renderComponent( 'tool-tip', { text: 'Owned Service', classes: 'app-tool-tip--small' }, @@ -54,6 +54,8 @@ function serviceToEntityRow(isAuthenticated) { ? [ { headers: 'owner', + isCentered: true, + classes: 'app-entity-table__cell--owned', entity: { kind: 'html', value: icon } } ] diff --git a/src/server/services/list/transformers/service-to-entity-row.test.js b/src/server/services/list/transformers/service-to-entity-row.test.js index 3b9349a0..e6f79b35 100644 --- a/src/server/services/list/transformers/service-to-entity-row.test.js +++ b/src/server/services/list/transformers/service-to-entity-row.test.js @@ -13,7 +13,9 @@ describe('#serviceToEntityRow', () => { kind: 'html', value: '' }, - headers: 'owner' + headers: 'owner', + isCentered: true, + classes: 'app-entity-table__cell--owned' }, { entity: { diff --git a/src/server/services/list/views/list.njk b/src/server/services/list/views/list.njk index e060be1f..d92649bf 100644 --- a/src/server/services/list/views/list.njk +++ b/src/server/services/list/views/list.njk @@ -9,7 +9,8 @@ {% endset %} {{ appPageHeading({ - text: "Services" + text: "Services", + intro: "Frontend and backend microservice details" }) }} {% call appFilters({ @@ -32,6 +33,7 @@ }, hiddenInputs: hiddenInputs }) %} +
{{ appAutocomplete({ id: "filters-service", @@ -67,6 +69,7 @@ suggestions: teamFilters }) }}
+ {% endcall %} {% block xhrContent %} diff --git a/src/server/services/secrets/views/environment.njk b/src/server/services/secrets/views/environment.njk index dadaa14c..c4a6c8f9 100644 --- a/src/server/services/secrets/views/environment.njk +++ b/src/server/services/secrets/views/environment.njk @@ -107,6 +107,7 @@ {% endblock %} {% block tabContent %} + {% call appSplitPane() %}
diff --git a/src/server/teams/controllers/teams-list.js b/src/server/teams/controllers/teams-list.js index 2eeebb49..6a2b31df 100644 --- a/src/server/teams/controllers/teams-list.js +++ b/src/server/teams/controllers/teams-list.js @@ -1,14 +1,59 @@ +import { validate as uuidValidate } from 'uuid' + import { fetchTeams } from '~/src/server/teams/helpers/fetch/fetch-teams.js' import { teamToEntityRow } from '~/src/server/teams/transformers/team-to-entity-row.js' +import { provideAuthedUser } from '~/src/server/common/helpers/auth/pre/provide-authed-user.js' + +function belongsToTeam(userScopeUUIDs) { + return (team) => ({ + isMemberOfTeam: userScopeUUIDs.includes(team.teamId), + ...team + }) +} + +function sortByTeam(a, b) { + if (a.isMemberOfTeam && !b.isMemberOfTeam) { + return -1 + } + if (!a.isMemberOfTeam && b.isMemberOfTeam) { + return 1 + } + + return a.name.localeCompare(b.name) +} const teamsListController = { + options: { + pre: [provideAuthedUser] + }, handler: async (request, h) => { + const authedUser = request.pre.authedUser + const isAuthenticated = authedUser?.isAuthenticated + const userScopeUUIDs = authedUser?.scope.filter(uuidValidate) ?? [] + const { teams } = await fetchTeams() - const entityRows = teams?.map(teamToEntityRow) + + const teamDecorator = belongsToTeam(userScopeUUIDs) + const rowBuilder = teamToEntityRow(isAuthenticated, userScopeUUIDs) + const rows = + teams?.map(teamDecorator).toSorted(sortByTeam).map(rowBuilder) ?? [] return h.view('teams/views/list', { pageTitle: 'Teams', - entityRows + tableData: { + headers: [ + ...(isAuthenticated + ? [{ id: 'member', classes: 'app-entity-table__cell--owned' }] + : []), + { id: 'name', text: 'Name', width: '15' }, + { id: 'github-team', text: 'GitHub Team', width: '15' }, + { id: 'user-count', text: 'Members', width: '5' }, + { id: 'updated', text: 'Last Updated', width: '30' }, + { id: 'created', text: 'Created', width: '35' } + ], + rows, + noResult: 'No running services found' + } }) } } diff --git a/src/server/teams/transformers/team-to-entity-row.js b/src/server/teams/transformers/team-to-entity-row.js index 6d1d3bc5..28a17922 100644 --- a/src/server/teams/transformers/team-to-entity-row.js +++ b/src/server/teams/transformers/team-to-entity-row.js @@ -1,35 +1,79 @@ import { config } from '~/src/config/config.js' +import { + renderComponent, + renderIcon +} from '~/src/server/common/helpers/nunjucks/render-component.js' -function teamToEntityRow(team) { - const githubOrg = config.get('githubOrg') +function teamToEntityRow(isAuthenticated) { + return (team) => { + const githubOrg = config.get('githubOrg') - return [ - { - kind: 'link', - value: team.name, - url: `/teams/${team.teamId}` - }, - { - kind: 'link', - value: team?.github ? `@${team.github}` : null, - url: team?.github - ? `https://github.com/orgs/${githubOrg}/teams/${team.github}` - : null, - newWindow: true - }, - { - kind: 'text', - value: team.users?.length - }, - { - kind: 'date', - value: team.updatedAt - }, - { - kind: 'date', - value: team.createdAt + const icon = team.isMemberOfTeam + ? renderComponent( + 'tool-tip', + { + text: 'My Team', + classes: 'app-tool-tip--small' + }, + [renderIcon('star-icon', { classes: 'app-icon--tiny' })] + ) + : '' + + return { + cells: [ + ...(isAuthenticated + ? [ + { + headers: 'member', + isCentered: true, + classes: 'app-entity-table__cell--owned', + entity: { kind: 'html', value: icon } + } + ] + : []), + { + headers: 'name', + entity: { + kind: 'link', + value: team.name, + url: `/teams/${team.teamId}` + } + }, + { + headers: 'github-team', + entity: { + kind: 'link', + value: team?.github ? `@${team.github}` : null, + url: team?.github + ? `https://github.com/orgs/${githubOrg}/teams/${team.github}` + : null, + newWindow: true + } + }, + { + headers: 'members', + entity: { + kind: 'text', + value: team.users?.length + } + }, + { + headers: 'updated', + entity: { + kind: 'date', + value: team.updatedAt + } + }, + { + headers: 'created', + entity: { + kind: 'date', + value: team.createdAt + } + } + ] } - ] + } } export { teamToEntityRow } diff --git a/src/server/teams/transformers/team-to-entity-row.test.js b/src/server/teams/transformers/team-to-entity-row.test.js index 7b1eb6a5..258def81 100644 --- a/src/server/teams/transformers/team-to-entity-row.test.js +++ b/src/server/teams/transformers/team-to-entity-row.test.js @@ -1,64 +1,162 @@ -import { config } from '~/src/config/config.js' import { teamToEntityRow } from '~/src/server/teams/transformers/team-to-entity-row.js' import { cdpTeamFixture } from '~/src/__fixtures__/admin/cdp-team.js' import { cdpTeamWithoutGithubFixture } from '~/src/__fixtures__/admin/cdp-team-without-github.js' -const githubOrg = config.get('githubOrg') - describe('#teamToEntityRow', () => { - test('Should provide expected team transformation', () => { - expect(teamToEntityRow(cdpTeamFixture.team)).toEqual([ - { - kind: 'link', - url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', - value: 'Platform' - }, - { - kind: 'link', - newWindow: true, - url: `https://github.com/orgs/${githubOrg}/teams/cdp-platform`, - value: '@cdp-platform' - }, - { - kind: 'text', - value: 2 - }, - { - kind: 'date', - value: '2023-10-03T11:11:31.085Z' - }, - { - kind: 'date', - value: '2023-09-28T12:52:14.673Z' - } - ]) + describe('When authenticated', () => { + test('Should provide expected team transformation', () => { + expect(teamToEntityRow(true)(cdpTeamFixture.team)).toEqual({ + cells: [ + { + entity: { + kind: 'html', + value: '' + }, + headers: 'member', + isCentered: true, + classes: 'app-entity-table__cell--owned' + }, + { + entity: { + kind: 'link', + url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', + value: 'Platform' + }, + headers: 'name' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/orgs/DEFRA/teams/cdp-platform', + value: '@cdp-platform' + }, + headers: 'github-team' + }, + { + entity: { + kind: 'text', + value: 2 + }, + headers: 'members' + }, + { + entity: { + kind: 'date', + value: '2023-10-03T11:11:31.085Z' + }, + headers: 'updated' + }, + { + entity: { + kind: 'date', + value: '2023-09-28T12:52:14.673Z' + }, + headers: 'created' + } + ] + }) + }) + + test('Should provide expected team transformation when team is not linked to a GitHub team', () => { + expect(teamToEntityRow(true)(cdpTeamWithoutGithubFixture.team)).toEqual({ + cells: [ + { + entity: { + kind: 'html', + value: '' + }, + headers: 'member', + isCentered: true, + classes: 'app-entity-table__cell--owned' + }, + { + entity: { + kind: 'link', + url: '/teams/47c04343-4c0e-4326-9848-bef7c1e2eedd', + value: 'Admin' + }, + headers: 'name' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: null, + value: null + }, + headers: 'github-team' + }, + { + entity: { + kind: 'text', + value: 4 + }, + headers: 'members' + }, + { + entity: { + kind: 'date', + value: '2023-08-30T08:03:23.657Z' + }, + headers: 'updated' + }, + { + entity: { + kind: 'date', + value: '2023-08-23T16:18:28.742Z' + }, + headers: 'created' + } + ] + }) + }) }) - test('Should provide expected team transformation when team is not linked to a GitHub team', () => { - expect(teamToEntityRow(cdpTeamWithoutGithubFixture.team)).toEqual([ - { - kind: 'link', - url: '/teams/47c04343-4c0e-4326-9848-bef7c1e2eedd', - value: 'Admin' - }, - { - kind: 'link', - newWindow: true, - url: null, - value: null - }, - { - kind: 'text', - value: 4 - }, - { - kind: 'date', - value: '2023-08-30T08:03:23.657Z' - }, - { - kind: 'date', - value: '2023-08-23T16:18:28.742Z' - } - ]) + describe('When not authenticated', () => { + test('Should provide expected team transformation without member cell', () => { + expect(teamToEntityRow(false)(cdpTeamFixture.team)).toEqual({ + cells: [ + { + entity: { + kind: 'link', + url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', + value: 'Platform' + }, + headers: 'name' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/orgs/DEFRA/teams/cdp-platform', + value: '@cdp-platform' + }, + headers: 'github-team' + }, + { + entity: { + kind: 'text', + value: 2 + }, + headers: 'members' + }, + { + entity: { + kind: 'date', + value: '2023-10-03T11:11:31.085Z' + }, + headers: 'updated' + }, + { + entity: { + kind: 'date', + value: '2023-09-28T12:52:14.673Z' + }, + headers: 'created' + } + ] + }) + }) }) }) diff --git a/src/server/teams/views/list.njk b/src/server/teams/views/list.njk index fb336259..0e40a890 100644 --- a/src/server/teams/views/list.njk +++ b/src/server/teams/views/list.njk @@ -2,19 +2,10 @@ {% block content %} {{ appPageHeading({ - text: "Teams" + text: "Teams", + intro: "Team details, members and assets" }) }} - {{ appEntityList({ - headings: [ - { text: "Team", size: "medium"}, - { text: "GitHub team", size: "medium"}, - { text: "Members", size: "small"}, - { text: "Last updated", size: "medium"}, - { text: "Created", size: "medium"} - ], - entityRows: entityRows, - noResult: "Currently there are no teams" - }) }} + {{ appEntityTable(tableData) }} {% endblock %} diff --git a/src/server/test-suites/controllers/test-suite-list.js b/src/server/test-suites/controllers/test-suite-list.js index e09c4e64..9eed0a1c 100644 --- a/src/server/test-suites/controllers/test-suite-list.js +++ b/src/server/test-suites/controllers/test-suite-list.js @@ -1,24 +1,52 @@ -import { sortBy } from '~/src/server/common/helpers/sort/sort-by.js' +import { validate as uuidValidate } from 'uuid' +import { sortByOwner } from '~/src/server/common/helpers/sort/sort-by-owner.js' import { fetchTestSuites } from '~/src/server/test-suites/helpers/fetch/index.js' import { fetchRepositories } from '~/src/server/common/helpers/fetch/fetch-repositories.js' import { testSuiteDecorator } from '~/src/server/test-suites/helpers/decorators/test-suite.js' -import { transformTestSuiteToEntityRow } from '~/src/server/test-suites/transformers/test-suite-to-entity-row.js' +import { provideAuthedUser } from '~/src/server/common/helpers/auth/pre/provide-authed-user.js' +import { testSuiteToEntityRow } from '~/src/server/test-suites/transformers/test-suite-to-entity-row.js' const testSuiteListController = { + options: { + pre: [provideAuthedUser] + }, handler: async (request, h) => { - const { repositories } = await fetchRepositories() - const testSuites = await fetchTestSuites() + const authedUser = request.pre.authedUser + const isAuthenticated = authedUser?.isAuthenticated + const userScopeUUIDs = authedUser?.scope.filter(uuidValidate) ?? [] - const entityRows = testSuites - .map(testSuiteDecorator(repositories)) - ?.sort(sortBy('serviceName', 'asc')) - ?.map(transformTestSuiteToEntityRow) + const [testSuites, { repositories }] = await Promise.all([ + fetchTestSuites(), + fetchRepositories() + ]) + + const decorator = testSuiteDecorator(repositories, userScopeUUIDs) + const rowBuilder = testSuiteToEntityRow(isAuthenticated) + const ownerSorter = sortByOwner('serviceName') + + const rows = testSuites + ?.map(decorator) + .toSorted(ownerSorter) + .map(rowBuilder) return h.view('test-suites/views/list', { pageTitle: 'Test Suites', - heading: 'Test Suites', - entityRows + tableData: { + headers: [ + ...(isAuthenticated + ? [{ id: 'owner', classes: 'app-entity-table__cell--owned' }] + : []), + { id: 'test-suite', text: 'Test Suite', width: '15' }, + { id: 'team', text: 'Team', width: '15' }, + { id: 'kind', text: 'Kind', width: '10' }, + { id: 'github-repository', text: 'GitHub Repository', width: '20' }, + { id: 'last-ran', text: 'Last Ran', width: '20' }, + { id: 'created', text: 'Created', width: '20' } + ], + rows, + noResult: 'No test suites found' + } }) } } diff --git a/src/server/test-suites/controllers/test-suite.js b/src/server/test-suites/controllers/test-suite.js index 255fd5de..afa0cdd7 100644 --- a/src/server/test-suites/controllers/test-suite.js +++ b/src/server/test-suites/controllers/test-suite.js @@ -1,13 +1,13 @@ import Joi from 'joi' import Boom from '@hapi/boom' -import { fetchTestRuns } from '~/src/server/test-suites/helpers/fetch/index.js' import { shouldPoll } from '~/src/server/test-suites/helpers/should-poll.js' +import { fetchTestRuns } from '~/src/server/test-suites/helpers/fetch/index.js' +import { provideCanRun } from '~/src/server/test-suites/helpers/pre/provide-can-run.js' import { provideTestSuite } from '~/src/server/test-suites/helpers/pre/provide-test-suite.js' -import { transformTestSuiteRunResults } from '~/src/server/test-suites/transformers/test-suite-run-results.js' +import { testSuiteRunResults } from '~/src/server/test-suites/transformers/test-suite-run-results.js' +import { transformTestSuiteToSummary } from '~/src/server/test-suites/transformers/test-suite-to-summary.js' import { provideEnvironmentOptions } from '~/src/server/test-suites/helpers/pre/provide-environment-options.js' -import { testSuiteToEntityDataList } from '~/src/server/test-suites/transformers/test-suite-to-entity-data-list.js' -import { provideCanRun } from '~/src/server/test-suites/helpers/pre/provide-can-run.js' const testSuiteController = { options: { @@ -26,21 +26,31 @@ const testSuiteController = { const serviceName = testSuite.serviceName const testRuns = await fetchTestRuns(serviceName) - - const testSuiteRunResults = testRuns.map((test) => - transformTestSuiteRunResults(test, canRun) - ) + const rows = testRuns.map((test) => testSuiteRunResults(test, canRun)) return h.view('test-suites/views/test-suite', { pageTitle: `Test Suite - ${serviceName}`, - heading: serviceName, testSuite, canRun, - entityDataList: testSuiteToEntityDataList(testSuite), + summaryList: transformTestSuiteToSummary(testSuite), environmentOptions, - testSuiteRunResults, owningTeamIds: testSuite.teams.map((testsuite) => testsuite.teamId), shouldPoll: shouldPoll(testRuns), + tableData: { + headers: [ + { id: 'version', text: 'Version', width: '5' }, + { id: 'environment', text: 'Environment', width: '15' }, + { id: 'status', text: 'Status', width: '5' }, + { id: 'logs', text: 'Logs', width: '15' }, + { id: 'results', text: 'Results', width: '10' }, + { id: 'user', text: 'User', width: '20' }, + { id: 'duration', text: 'Duration', width: '5' }, + { id: 'last-ran', text: 'Last Ran', width: '15' }, + { id: 'action', text: 'Action', width: '10' } + ], + rows, + noResult: 'No test suite run results found' + }, breadcrumbs: [ { text: 'Test suites', diff --git a/src/server/test-suites/helpers/decorators/test-suite.js b/src/server/test-suites/helpers/decorators/test-suite.js index 9c4e3ea4..5eebf7b8 100644 --- a/src/server/test-suites/helpers/decorators/test-suite.js +++ b/src/server/test-suites/helpers/decorators/test-suite.js @@ -1,14 +1,19 @@ import { repositoriesDecorator } from '~/src/server/common/helpers/decorators/repositories.js' import { provideTestType } from '~/src/server/test-suites/helpers/provide-test-type.js' -function testSuiteDecorator(repositories) { - return function addDetail({ testSuite, lastRun }) { - const testSuiteWithRepo = repositoriesDecorator(repositories)(testSuite) +function testSuiteDecorator(repositories, userScopeUUIDs) { + const decorator = repositoriesDecorator(repositories) + + return ({ testSuite, lastRun }) => { + const testSuiteWithRepo = decorator(testSuite) return { ...testSuiteWithRepo, lastRun, - testType: provideTestType(testSuiteWithRepo.topics) + testType: provideTestType(testSuiteWithRepo.topics), + isOwner: testSuiteWithRepo?.teams.some((team) => + userScopeUUIDs.includes(team.teamId) + ) } } } diff --git a/src/server/test-suites/transformers/test-suite-run-results.js b/src/server/test-suites/transformers/test-suite-run-results.js index 84a87b07..fa3d929d 100644 --- a/src/server/test-suites/transformers/test-suite-run-results.js +++ b/src/server/test-suites/transformers/test-suite-run-results.js @@ -8,6 +8,7 @@ import { import { provideTestRunStatusClassname } from '~/src/server/test-suites/helpers/provide-test-run-status-classname.js' import { buildLogsLink } from '~/src/server/test-suites/helpers/build-logs-link.js' import { getTestStatusIcon } from '~/src/server/test-suites/helpers/get-test-status-icon.js' +import { formatText } from '~/src/config/nunjucks/filters/index.js' function getDuration({ created, taskLastUpdated }, hasResult) { if (created && taskLastUpdated && hasResult) { @@ -17,7 +18,7 @@ function getDuration({ created, taskLastUpdated }, hasResult) { return null } -function transformTestSuiteRunResults(testRun, canRun) { +function testSuiteRunResults(testRun, canRun) { const runTaskStatus = testRun.taskStatus?.toLowerCase() const runTestStatus = testRun.testStatus?.toLowerCase() @@ -34,48 +35,80 @@ function transformTestSuiteRunResults(testRun, canRun) { testRun.taskLastUpdated ].every(Boolean) - return [ - { - kind: 'link', - value: testRun.tag, - url: `https://github.com/DEFRA/${testRun.testSuite}/releases/tag/${testRun.tag}`, - newWindow: true - }, - { - kind: 'text', - value: startCase(testRun.environment) - }, - { - kind: 'tag', - value: testRun.taskStatus, - classes: provideTestRunStatusClassname(testRun.taskStatus), - showLoader: inProgress - }, - { - kind: 'link', - value: logsLinkDataAvailable ? `logs.${testRun.environment}` : null, - url: logsLinkDataAvailable && buildLogsLink(testRun, hasResult), - newWindow: true - }, - { - kind: 'link', - value: hasResult ? 'Report' : null, - url: `/test-suites/test-results/${testRun.environment}/${testRun.tag}/${testRun.testSuite}/${testRun.runId}/index.html`, - icon: getTestStatusIcon(runTestStatus) - }, - { - kind: 'text', - value: testRun.user.displayName - }, - { kind: 'text', value: getDuration(testRun, hasResult) }, - { kind: 'date', value: testRun.taskLastUpdated }, - { - kind: 'button', - classes: 'app-button--small', - value: canRun && runTaskStatus === taskStatus.inProgress ? 'Stop' : null, - url: `/test-suites/${testRun.testSuite}/${testRun.runId}/stop` - } - ] + return { + cells: [ + { + headers: 'version', + entity: { + kind: 'link', + value: testRun.tag, + url: `https://github.com/DEFRA/${testRun.testSuite}/releases/tag/${testRun.tag}`, + newWindow: true + } + }, + { + headers: 'environment', + entity: { + kind: 'text', + value: startCase(testRun.environment) + } + }, + { + headers: 'status', + entity: { + kind: 'tag', + value: formatText(testRun.taskStatus), + classes: provideTestRunStatusClassname(testRun.taskStatus), + showLoader: inProgress + } + }, + { + headers: 'logs', + entity: { + kind: 'link', + value: logsLinkDataAvailable ? `logs.${testRun.environment}` : null, + url: logsLinkDataAvailable && buildLogsLink(testRun, hasResult), + newWindow: true + } + }, + { + headers: 'results', + isCentered: true, + classes: 'app-entity-table__cell--owned', + entity: { + kind: 'link', + value: hasResult ? 'Report' : null, + url: `/test-suites/test-results/${testRun.environment}/${testRun.tag}/${testRun.testSuite}/${testRun.runId}/index.html`, + icon: getTestStatusIcon(runTestStatus) + } + }, + { + headers: 'user', + entity: { + kind: 'text', + value: testRun.user.displayName + } + }, + { + headers: 'duration', + entity: { kind: 'text', value: getDuration(testRun, hasResult) } + }, + { + headers: 'last-ran', + entity: { kind: 'date', value: testRun.taskLastUpdated } + }, + { + headers: 'action', + entity: { + kind: 'button', + classes: 'app-button--small', + value: + canRun && runTaskStatus === taskStatus.inProgress ? 'Stop' : null, + url: `/test-suites/${testRun.testSuite}/${testRun.runId}/stop` + } + } + ] + } } -export { transformTestSuiteRunResults } +export { testSuiteRunResults } diff --git a/src/server/test-suites/transformers/test-suite-run-results.test.js b/src/server/test-suites/transformers/test-suite-run-results.test.js index c3774205..4b25d596 100644 --- a/src/server/test-suites/transformers/test-suite-run-results.test.js +++ b/src/server/test-suites/transformers/test-suite-run-results.test.js @@ -1,108 +1,170 @@ -import { transformTestSuiteRunResults } from '~/src/server/test-suites/transformers/test-suite-run-results.js' +import { testSuiteRunResults } from '~/src/server/test-suites/transformers/test-suite-run-results.js' import { testSuiteRunsFixture } from '~/src/__fixtures__/test-suite-runs.js' describe('#transformTestSuiteRunResults', () => { describe('When a user can NOT run the tests', () => { test('Should provide expected test suite run transformation without action buttons', () => { expect( - testSuiteRunsFixture.map((t) => transformTestSuiteRunResults(t, false)) + testSuiteRunsFixture.map((t) => testSuiteRunResults(t, false)) ).toEqual([ - [ - { - kind: 'link', - newWindow: true, - url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.2.0', - value: '0.2.0' - }, - { - kind: 'text', - value: 'Infra Dev' - }, - { - classes: 'govuk-tag--light-blue', - kind: 'tag', - showLoader: true, - value: 'in-progress' - }, - { - kind: 'link', - newWindow: true, - url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-27T10:29:32.000',to:'2024-02-27T10:31:36.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2Ff5cffc31e21149208f38b8ec2b168c50'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", - value: 'logs.infra-dev' - }, - { - icon: expect.stringContaining('Test passed'), - kind: 'link', - url: '/test-suites/test-results/infra-dev/0.2.0/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/index.html', - value: 'Report' - }, - { - kind: 'text', - value: 'B. A. Baracus' - }, - { - kind: 'text', - value: '2 minutes' - }, - { - kind: 'date', - value: '2024-02-27T10:31:36Z' - }, - { - classes: 'app-button--small', - kind: 'button', - value: null, - url: '/test-suites/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/stop' - } - ], - [ - { - kind: 'link', - newWindow: true, - url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.1.0', - value: '0.1.0' - }, - { - kind: 'text', - value: 'Infra Dev' - }, - { - classes: 'govuk-tag--green', - kind: 'tag', - showLoader: false, - value: 'finished' - }, - { - kind: 'link', - newWindow: true, - url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-26T16:38:28.000',to:'2024-02-26T16:40:34.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2F7e4c74aa41e44a0399bef08711563715'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", - value: 'logs.infra-dev' - }, - { - icon: expect.stringContaining('Test passed'), - kind: 'link', - url: '/test-suites/test-results/infra-dev/0.1.0/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/index.html', - value: 'Report' - }, - { - kind: 'text', - value: 'B. A. Baracus' - }, - { - kind: 'text', - value: '2 minutes' - }, - { - kind: 'date', - value: '2024-02-26T16:40:34Z' - }, - { - kind: 'button', - classes: 'app-button--small', - url: '/test-suites/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/stop', - value: null - } - ] + { + cells: [ + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.2.0', + value: '0.2.0' + }, + headers: 'version' + }, + { + entity: { + kind: 'text', + value: 'Infra Dev' + }, + headers: 'environment' + }, + { + entity: { + classes: 'govuk-tag--light-blue', + kind: 'tag', + showLoader: true, + value: 'In-progress' + }, + headers: 'status' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-27T10:29:32.000',to:'2024-02-27T10:31:36.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2Ff5cffc31e21149208f38b8ec2b168c50'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", + value: 'logs.infra-dev' + }, + headers: 'logs' + }, + { + classes: 'app-entity-table__cell--owned', + entity: { + icon: expect.stringContaining('app-tick-icon'), + kind: 'link', + url: '/test-suites/test-results/infra-dev/0.2.0/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/index.html', + value: 'Report' + }, + headers: 'results', + isCentered: true + }, + { + entity: { + kind: 'text', + value: 'B. A. Baracus' + }, + headers: 'user' + }, + { + entity: { + kind: 'text', + value: '2 minutes' + }, + headers: 'duration' + }, + { + entity: { + kind: 'date', + value: '2024-02-27T10:31:36Z' + }, + headers: 'last-ran' + }, + { + entity: { + classes: 'app-button--small', + kind: 'button', + url: '/test-suites/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/stop', + value: null + }, + headers: 'action' + } + ] + }, + { + cells: [ + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.1.0', + value: '0.1.0' + }, + headers: 'version' + }, + { + entity: { + kind: 'text', + value: 'Infra Dev' + }, + headers: 'environment' + }, + { + entity: { + classes: 'govuk-tag--green', + kind: 'tag', + showLoader: false, + value: 'Finished' + }, + headers: 'status' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-26T16:38:28.000',to:'2024-02-26T16:40:34.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2F7e4c74aa41e44a0399bef08711563715'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", + value: 'logs.infra-dev' + }, + headers: 'logs' + }, + { + classes: 'app-entity-table__cell--owned', + entity: { + icon: expect.stringContaining('app-tick-icon'), + kind: 'link', + url: '/test-suites/test-results/infra-dev/0.1.0/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/index.html', + value: 'Report' + }, + headers: 'results', + isCentered: true + }, + { + entity: { + kind: 'text', + value: 'B. A. Baracus' + }, + headers: 'user' + }, + { + entity: { + kind: 'text', + value: '2 minutes' + }, + headers: 'duration' + }, + { + entity: { + kind: 'date', + value: '2024-02-26T16:40:34Z' + }, + headers: 'last-ran' + }, + { + entity: { + classes: 'app-button--small', + kind: 'button', + url: '/test-suites/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/stop', + value: null + }, + headers: 'action' + } + ] + } ]) }) }) @@ -110,104 +172,166 @@ describe('#transformTestSuiteRunResults', () => { describe('When a user can run the tests', () => { test('Should provide expected test suite run transformation with action buttons', () => { expect( - testSuiteRunsFixture.map((t) => transformTestSuiteRunResults(t, true)) + testSuiteRunsFixture.map((t) => testSuiteRunResults(t, true)) ).toEqual([ - [ - { - kind: 'link', - newWindow: true, - url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.2.0', - value: '0.2.0' - }, - { - kind: 'text', - value: 'Infra Dev' - }, - { - classes: 'govuk-tag--light-blue', - kind: 'tag', - showLoader: true, - value: 'in-progress' - }, - { - kind: 'link', - newWindow: true, - url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-27T10:29:32.000',to:'2024-02-27T10:31:36.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2Ff5cffc31e21149208f38b8ec2b168c50'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", - value: 'logs.infra-dev' - }, - { - icon: expect.stringContaining('Test passed'), - kind: 'link', - url: '/test-suites/test-results/infra-dev/0.2.0/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/index.html', - value: 'Report' - }, - { - kind: 'text', - value: 'B. A. Baracus' - }, - { - kind: 'text', - value: '2 minutes' - }, - { - kind: 'date', - value: '2024-02-27T10:31:36Z' - }, - { - classes: 'app-button--small', - kind: 'button', - value: 'Stop', - url: '/test-suites/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/stop' - } - ], - [ - { - kind: 'link', - newWindow: true, - url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.1.0', - value: '0.1.0' - }, - { - kind: 'text', - value: 'Infra Dev' - }, - { - classes: 'govuk-tag--green', - kind: 'tag', - showLoader: false, - value: 'finished' - }, - { - kind: 'link', - newWindow: true, - url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-26T16:38:28.000',to:'2024-02-26T16:40:34.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2F7e4c74aa41e44a0399bef08711563715'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", - value: 'logs.infra-dev' - }, - { - icon: expect.stringContaining('Test passed'), - kind: 'link', - url: '/test-suites/test-results/infra-dev/0.1.0/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/index.html', - value: 'Report' - }, - { - kind: 'text', - value: 'B. A. Baracus' - }, - { - kind: 'text', - value: '2 minutes' - }, - { - kind: 'date', - value: '2024-02-26T16:40:34Z' - }, - { - kind: 'button', - classes: 'app-button--small', - url: '/test-suites/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/stop', - value: null - } - ] + { + cells: [ + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.2.0', + value: '0.2.0' + }, + headers: 'version' + }, + { + entity: { + kind: 'text', + value: 'Infra Dev' + }, + headers: 'environment' + }, + { + entity: { + classes: 'govuk-tag--light-blue', + kind: 'tag', + showLoader: true, + value: 'In-progress' + }, + headers: 'status' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-27T10:29:32.000',to:'2024-02-27T10:31:36.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2Ff5cffc31e21149208f38b8ec2b168c50'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", + value: 'logs.infra-dev' + }, + headers: 'logs' + }, + { + classes: 'app-entity-table__cell--owned', + entity: { + icon: expect.stringContaining('app-tick-icon'), + kind: 'link', + url: '/test-suites/test-results/infra-dev/0.2.0/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/index.html', + value: 'Report' + }, + headers: 'results', + isCentered: true + }, + { + entity: { + kind: 'text', + value: 'B. A. Baracus' + }, + headers: 'user' + }, + { + entity: { + kind: 'text', + value: '2 minutes' + }, + headers: 'duration' + }, + { + entity: { + kind: 'date', + value: '2024-02-27T10:31:36Z' + }, + headers: 'last-ran' + }, + { + entity: { + classes: 'app-button--small', + kind: 'button', + url: '/test-suites/cdp-portal-smoke-tests/383547d8-f71c-4e7e-8b03-4ddf09fd84fe/stop', + value: 'Stop' + }, + headers: 'action' + } + ] + }, + { + cells: [ + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-portal-smoke-tests/releases/tag/0.1.0', + value: '0.1.0' + }, + headers: 'version' + }, + { + entity: { + kind: 'text', + value: 'Infra Dev' + }, + headers: 'environment' + }, + { + entity: { + classes: 'govuk-tag--green', + kind: 'tag', + showLoader: false, + value: 'Finished' + }, + headers: 'status' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: "https://logs.infra-dev.cdp-int.defra.cloud/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-02-26T16:38:28.000',to:'2024-02-26T16:40:34.000'))&_a=(columns:!(_source),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:'ecs_task_arn:arn:aws:ecs:eu-west-2:123456789:task%2Finfra-dev-ecs-public%2F7e4c74aa41e44a0399bef08711563715'),sort:!())&_a=(columns:!(log),filters:!(),index:c0abdf20-d49c-11ee-9eac-1d3409bea15a,interval:auto,query:(language:kuery,query:''),sort:!())", + value: 'logs.infra-dev' + }, + headers: 'logs' + }, + { + classes: 'app-entity-table__cell--owned', + entity: { + icon: expect.stringContaining('app-tick-icon'), + kind: 'link', + url: '/test-suites/test-results/infra-dev/0.1.0/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/index.html', + value: 'Report' + }, + headers: 'results', + isCentered: true + }, + { + entity: { + kind: 'text', + value: 'B. A. Baracus' + }, + headers: 'user' + }, + { + entity: { + kind: 'text', + value: '2 minutes' + }, + headers: 'duration' + }, + { + entity: { + kind: 'date', + value: '2024-02-26T16:40:34Z' + }, + headers: 'last-ran' + }, + { + entity: { + classes: 'app-button--small', + kind: 'button', + url: '/test-suites/cdp-portal-smoke-tests/dc34cdaf-1f51-44cf-8c63-e9b6800d9609/stop', + value: null + }, + headers: 'action' + } + ] + } ]) }) }) diff --git a/src/server/test-suites/transformers/test-suite-to-entity-row.js b/src/server/test-suites/transformers/test-suite-to-entity-row.js index 197e0e9a..c1e4525f 100644 --- a/src/server/test-suites/transformers/test-suite-to-entity-row.js +++ b/src/server/test-suites/transformers/test-suite-to-entity-row.js @@ -1,40 +1,96 @@ import { config } from '~/src/config/config.js' +import { + renderComponent, + renderIcon +} from '~/src/server/common/helpers/nunjucks/render-component.js' -function transformTestSuiteToEntityRow(testSuite) { - const githubOrg = config.get('githubOrg') - const teams = testSuite?.teams - ?.filter((team) => team.teamId) - ?.map((team) => ({ - kind: 'link', - value: team.name, - url: `/teams/${team.teamId}` - })) +function testSuiteToEntityRow(isAuthenticated) { + return (testSuite) => { + const githubOrg = config.get('githubOrg') - return [ - { - kind: 'link', - value: testSuite.serviceName, - url: `/test-suites/${testSuite.serviceName}` - }, - { - kind: 'group', - value: teams?.length ? teams : null - }, - { - kind: 'text', - value: testSuite.testType - }, - { - kind: 'link', - value: `${githubOrg}/${testSuite.id}`, - url: `https://github.com/${githubOrg}/${testSuite.id}`, - newWindow: true - }, - { - kind: 'date', - value: testSuite.lastRun?.taskLastUpdated + const icon = testSuite.isOwner + ? renderComponent( + 'tool-tip', + { + text: 'Owned Test Suite', + classes: 'app-tool-tip--small' + }, + [ + renderIcon('star-icon', { + classes: 'app-icon--tiny' + }) + ] + ) + : '' + + const teams = testSuite?.teams + ?.filter((team) => team.teamId) + ?.map((team) => ({ + kind: 'link', + value: team.name, + url: `/teams/${team.teamId}` + })) + + return { + cells: [ + ...(isAuthenticated + ? [ + { + headers: 'owner', + isCentered: true, + classes: 'app-entity-table__cell--owned', + entity: { kind: 'html', value: icon } + } + ] + : []), + { + headers: 'test-suite', + entity: { + kind: 'link', + value: testSuite.serviceName, + url: `/test-suites/${testSuite.serviceName}` + } + }, + { + headers: 'team', + entity: { + kind: 'group', + value: teams?.length ? teams : null + } + }, + { + headers: 'kind', + entity: { + kind: 'text', + value: testSuite.testType + } + }, + { + headers: 'github-repository', + entity: { + kind: 'link', + value: `${githubOrg}/${testSuite.id}`, + url: `https://github.com/${githubOrg}/${testSuite.id}`, + newWindow: true + } + }, + { + headers: 'last-ran', + entity: { + kind: 'date', + value: testSuite.lastRun?.taskLastUpdated + } + }, + { + headers: 'created', + entity: { + kind: 'date', + value: testSuite.createdAt + } + } + ] } - ] + } } -export { transformTestSuiteToEntityRow } +export { testSuiteToEntityRow } diff --git a/src/server/test-suites/transformers/test-suite-to-entity-row.test.js b/src/server/test-suites/transformers/test-suite-to-entity-row.test.js index 5c18121d..9215f81c 100644 --- a/src/server/test-suites/transformers/test-suite-to-entity-row.test.js +++ b/src/server/test-suites/transformers/test-suite-to-entity-row.test.js @@ -1,41 +1,131 @@ -import { config } from '~/src/config/config.js' import { testSuiteWithLastRunFixture } from '~/src/__fixtures__/test-suite.js' -import { transformTestSuiteToEntityRow } from '~/src/server/test-suites/transformers/test-suite-to-entity-row.js' - -const githubOrg = config.get('githubOrg') +import { testSuiteToEntityRow } from '~/src/server/test-suites/transformers/test-suite-to-entity-row.js' describe('#transformServiceToEntityRow', () => { - test('Should provide expected service entity row transformation', () => { - expect(transformTestSuiteToEntityRow(testSuiteWithLastRunFixture)).toEqual([ - { - kind: 'link', - url: '/test-suites/cdp-portal-smoke-tests', - value: 'cdp-portal-smoke-tests' - }, - { - kind: 'group', - value: [ - { - kind: 'link', - url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', - value: 'Platform' + describe('When authenticated', () => { + test('Should provide expected service entity row transformation', () => { + expect(testSuiteToEntityRow(true)(testSuiteWithLastRunFixture)).toEqual({ + cells: [ + { + classes: 'app-entity-table__cell--owned', + entity: { + kind: 'html', + value: '' + }, + headers: 'owner', + isCentered: true + }, + { + entity: { + kind: 'link', + url: '/test-suites/cdp-portal-smoke-tests', + value: 'cdp-portal-smoke-tests' + }, + headers: 'test-suite' + }, + { + entity: { + kind: 'group', + value: [ + { + kind: 'link', + url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', + value: 'Platform' + } + ] + }, + headers: 'team' + }, + { + entity: { + kind: 'text', + value: 'Smoke' + }, + headers: 'kind' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-portal-smoke-tests', + value: 'DEFRA/cdp-portal-smoke-tests' + }, + headers: 'github-repository' + }, + { + entity: { + kind: 'date', + value: '2023-04-12T17:18:48Z' + }, + headers: 'last-ran' + }, + { + entity: { + kind: 'date' + }, + headers: 'created' + } + ] + }) + }) + }) + + describe('When not authenticated', () => { + test('Should provide expected service entity row transformation', () => { + expect(testSuiteToEntityRow(false)(testSuiteWithLastRunFixture)).toEqual({ + cells: [ + { + entity: { + kind: 'link', + url: '/test-suites/cdp-portal-smoke-tests', + value: 'cdp-portal-smoke-tests' + }, + headers: 'test-suite' + }, + { + entity: { + kind: 'group', + value: [ + { + kind: 'link', + url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', + value: 'Platform' + } + ] + }, + headers: 'team' + }, + { + entity: { + kind: 'text', + value: 'Smoke' + }, + headers: 'kind' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-portal-smoke-tests', + value: 'DEFRA/cdp-portal-smoke-tests' + }, + headers: 'github-repository' + }, + { + entity: { + kind: 'date', + value: '2023-04-12T17:18:48Z' + }, + headers: 'last-ran' + }, + { + entity: { + kind: 'date' + }, + headers: 'created' } ] - }, - { - kind: 'text', - value: 'Smoke' - }, - { - kind: 'link', - newWindow: true, - url: `https://github.com/${githubOrg}/cdp-portal-smoke-tests`, - value: `${githubOrg}/cdp-portal-smoke-tests` - }, - { - kind: 'date', - value: '2023-04-12T17:18:48Z' - } - ]) + }) + }) }) }) diff --git a/src/server/test-suites/transformers/test-suite-to-summary.js b/src/server/test-suites/transformers/test-suite-to-summary.js new file mode 100644 index 00000000..63e32ab5 --- /dev/null +++ b/src/server/test-suites/transformers/test-suite-to-summary.js @@ -0,0 +1,61 @@ +import { buildLink } from '~/src/server/common/helpers/view/build-link.js' +import { buildList } from '~/src/server/common/helpers/view/build-list.js' +import { noValue } from '~/src/server/common/constants/no-value.js' +import { renderComponent } from '~/src/server/common/helpers/nunjucks/render-component.js' + +function transformTestSuiteToSummary(testSuite) { + const teams = testSuite?.teams + ?.filter((team) => team.teamId) + ?.map((team) => buildLink(`/teams/${team.teamId}`, team.name, false)) + + const topics = testSuite?.topics?.map((topic) => + renderComponent('tag', { + text: topic, + url: `https://github.com/search?q=topic%3Acdp+org%3ADEFRA+topic%3A${topic}&type=repositories`, + newWindow: true, + link: { classes: 'app-link--without-underline' }, + attributes: { 'data-testid': 'govuk-tag' } + }).trim() + ) + + return { + classes: 'app-summary-list govuk-!-margin-bottom-8', + attributes: { + 'data-testid': 'govuk-summary-list' + }, + rows: [ + { + key: { text: `Team${teams?.length > 1 ? 's' : ''}` }, + value: { + html: teams?.length ? buildList(teams) : noValue + } + }, + { + key: { text: 'Primary Language' }, + value: { text: testSuite.primaryLanguage ?? noValue } + }, + { + key: { + text: 'GitHub Repository' + }, + value: { + html: buildLink(testSuite.githubUrl) + } + }, + { + key: { text: 'Topics' }, + value: { + html: topics?.length ? topics.join(' ') : noValue + } + }, + { + key: { text: 'Created' }, + value: { + html: renderComponent('time', { datetime: testSuite.createdAt }) + } + } + ] + } +} + +export { transformTestSuiteToSummary } diff --git a/src/server/test-suites/transformers/test-suite-to-summary.test.js b/src/server/test-suites/transformers/test-suite-to-summary.test.js new file mode 100644 index 00000000..c47f697f --- /dev/null +++ b/src/server/test-suites/transformers/test-suite-to-summary.test.js @@ -0,0 +1,65 @@ +import { testSuiteFixture } from '~/src/__fixtures__/test-suite.js' +import { repositoryDecorator } from '~/src/server/common/helpers/decorators/repository.js' +import { repositoryFixture } from '~/src/__fixtures__/repository.js' +import { transformTestSuiteToSummary } from '~/src/server/test-suites/transformers/test-suite-to-summary.js' + +describe('#testSuiteToEntityDataList', () => { + describe('With a test suite', () => { + test('Should provide expected test suite summary transformation', () => { + expect( + transformTestSuiteToSummary( + repositoryDecorator(testSuiteFixture, repositoryFixture.repository) + ) + ).toEqual({ + attributes: { + 'data-testid': 'govuk-summary-list' + }, + classes: 'app-summary-list govuk-!-margin-bottom-8', + rows: [ + { + key: { + text: 'Team' + }, + value: { + html: expect.stringContaining('Platform') + } + }, + { + key: { + text: 'Primary Language' + }, + value: { + text: 'JavaScript' + } + }, + { + key: { + text: 'GitHub Repository' + }, + value: { + html: expect.stringContaining( + 'https://github.com/DEFRA/cdp-portal-smoke-tests' + ) + } + }, + { + key: { + text: 'Topics' + }, + value: { + html: expect.stringContaining('frontend') + } + }, + { + key: { + text: 'Created' + }, + value: { + html: expect.stringContaining('Wed 12th Apr 2023 at 17:16') + } + } + ] + }) + }) + }) +}) diff --git a/src/server/test-suites/views/list.njk b/src/server/test-suites/views/list.njk index 2f2bb0f0..941e8d9b 100644 --- a/src/server/test-suites/views/list.njk +++ b/src/server/test-suites/views/list.njk @@ -1,21 +1,11 @@ {% extends "layouts/page.njk" %} {% block content %} - {{ appHeading({ - title: heading, - caption: "Environment, User Journey and Performance tests." + {{ appPageHeading({ + text: "Test Suites", + intro: "User Journey and Performance tests" }) }} - {{ appEntityList({ - headings: [ - { text: "Test Suite", size: "large" }, - { text: "Team", size: "medium" }, - { text: "Type", size: "small" }, - { text: "GitHub Repository", size: "massive" }, - { text: "Last Ran", size: "medium" } - ], - entityRows: entityRows, - noResult: "Currently there are no test suites" - }) }} + {{ appEntityTable(tableData) }} {% endblock %} diff --git a/src/server/test-suites/views/test-suite.njk b/src/server/test-suites/views/test-suite.njk index e5f3503f..8f964b15 100644 --- a/src/server/test-suites/views/test-suite.njk +++ b/src/server/test-suites/views/test-suite.njk @@ -2,32 +2,30 @@ {% block content %} - {{ appHeading({ - title: heading, - caption: "Information about the " + heading + " test-suite." + {{ appPageHeading({ + caption: "Test Suite", + text: testSuite.serviceName }) }} -
+
-
+
-

About

{{ testSuite.description }}

-
+ {{ govukSummaryList(summaryList) }} +
- {% if canRun %} - {% call appPanel() %}
@@ -81,9 +79,7 @@
{% endcall %} - {% endif %} -
@@ -102,22 +98,7 @@ Results from {{ testSuite.serviceName }} test suite runs.

- {{ appEntityList({ - headings: [ - { text: "Version", size: "tiny" }, - { text: "Environment", size: "small" }, - { text: "Status", size: "small" }, - { text: "Logs", size: "small" }, - { text: "Results", size: "small" }, - { text: "User", size: "large" }, - { text: "Duration", size: "small" }, - { text: "Last Ran", size: "medium" }, - { text: "Action", size: "small"} - ], - entityRows: testSuiteRunResults, - noResult: "Currently there are no test suite run results" - }) }} - + {{ appEntityTable(tableData) }} {% endblock %} @@ -127,14 +108,5 @@ {% endif %}
- -
- - {{ appEntityDataList({ - heading: "Details", - items: entityDataList - }) }} - -
{% endblock %} diff --git a/src/server/utilities/controllers/libraries-list-controller.js b/src/server/utilities/controllers/libraries-list-controller.js index bc142c0a..9be47da7 100644 --- a/src/server/utilities/controllers/libraries-list-controller.js +++ b/src/server/utilities/controllers/libraries-list-controller.js @@ -1,20 +1,48 @@ -import { sortBy } from '~/src/server/common/helpers/sort/sort-by.js' +import { validate as uuidValidate } from 'uuid' + import { fetchLibraries } from '~/src/server/utilities/helpers/fetch/fetch-libraries.js' -import { utilityToEntityRow } from '~/src/server/utilities/transformers/utility-to-entity-row.js' +import { provideAuthedUser } from '~/src/server/common/helpers/auth/pre/provide-authed-user.js' +import { buildUtilitiesTableData } from '~/src/server/utilities/helpers/build-utilities-table-data.js' const librariesListController = { - handler: async (_request, h) => { - const { repositories } = await fetchLibraries() + options: { + pre: [provideAuthedUser] + }, + handler: async (request, h) => { + const authedUser = request.pre.authedUser + const isAuthenticated = authedUser?.isAuthenticated + const userScopeUUIDs = authedUser?.scope.filter(uuidValidate) ?? [] + + const { repositories: libraries } = await fetchLibraries() - const entityRows = repositories - ?.sort(sortBy('id', 'asc')) - ?.map(utilityToEntityRow('libraries')) + const rows = buildUtilitiesTableData({ + utilities: libraries, + utilityType: 'libraries', + isAuthenticated, + userScopeUUIDs + }) + const title = 'Libraries' return h.view('utilities/views/list', { - pageTitle: 'Libraries', - heading: 'Libraries', - entityRows, - noResult: 'Currently there are no libraries' + pageTitle: title, + pageHeading: { + text: title, + intro: 'Microservice and test-suite libraries' + }, + tableData: { + headers: [ + ...(isAuthenticated + ? [{ id: 'owner', classes: 'app-entity-table__cell--owned' }] + : []), + { id: 'utility', text: 'Utility', width: '20' }, + { id: 'team', text: 'Team', width: '15' }, + { id: 'language', text: 'Language', width: '10' }, + { id: 'github-repository', text: 'GitHub Repository', width: '20' }, + { id: 'created', text: 'Created', width: '30' } + ], + rows, + noResult: 'No libraries found' + } }) } } diff --git a/src/server/utilities/controllers/templates-list-controller.js b/src/server/utilities/controllers/templates-list-controller.js index 37594c53..41bb0a9c 100644 --- a/src/server/utilities/controllers/templates-list-controller.js +++ b/src/server/utilities/controllers/templates-list-controller.js @@ -1,20 +1,48 @@ -import { sortBy } from '~/src/server/common/helpers/sort/sort-by.js' +import { validate as uuidValidate } from 'uuid' + import { fetchTemplates } from '~/src/server/utilities/helpers/fetch/fetch-templates.js' -import { utilityToEntityRow } from '~/src/server/utilities/transformers/utility-to-entity-row.js' +import { provideAuthedUser } from '~/src/server/common/helpers/auth/pre/provide-authed-user.js' +import { buildUtilitiesTableData } from '~/src/server/utilities/helpers/build-utilities-table-data.js' const templatesListController = { - handler: async (_request, h) => { - const { repositories } = await fetchTemplates() + options: { + pre: [provideAuthedUser] + }, + handler: async (request, h) => { + const authedUser = request.pre.authedUser + const isAuthenticated = authedUser?.isAuthenticated + const userScopeUUIDs = authedUser?.scope.filter(uuidValidate) ?? [] + + const { repositories: templates } = await fetchTemplates() - const entityRows = repositories - ?.sort(sortBy('id', 'asc')) - ?.map(utilityToEntityRow('templates')) + const rows = buildUtilitiesTableData({ + utilities: templates, + utilityType: 'templates', + isAuthenticated, + userScopeUUIDs + }) + const title = 'Templates' return h.view('utilities/views/list', { - pageTitle: 'Templates', - heading: 'Templates', - entityRows, - noResult: 'Currently there are no templates' + pageTitle: title, + pageHeading: { + text: title, + intro: 'Microservice and test-suite templates' + }, + tableData: { + headers: [ + ...(isAuthenticated + ? [{ id: 'owner', classes: 'app-entity-table__cell--owned' }] + : []), + { id: 'utility', text: 'Utility', width: '20' }, + { id: 'team', text: 'Team', width: '15' }, + { id: 'language', text: 'Language', width: '10' }, + { id: 'github-repository', text: 'GitHub Repository', width: '20' }, + { id: 'created', text: 'Created', width: '30' } + ], + rows, + noResult: 'No templates found' + } }) } } diff --git a/src/server/utilities/helpers/build-utilities-table-data.js b/src/server/utilities/helpers/build-utilities-table-data.js new file mode 100644 index 00000000..5f565fe2 --- /dev/null +++ b/src/server/utilities/helpers/build-utilities-table-data.js @@ -0,0 +1,26 @@ +import { utilityToEntityRow } from '~/src/server/utilities/transformers/utility-to-entity-row.js' +import { sortByOwner } from '~/src/server/common/helpers/sort/sort-by-owner.js' + +function buildUtilitiesTableData({ + utilities, + utilityType, + isAuthenticated, + userScopeUUIDs +}) { + const rowDecorator = utilityToEntityRow(utilityType, isAuthenticated) + const ownerSorter = sortByOwner('id') + + const rows = utilities + .map((utility) => ({ + ...utility, + isOwner: utility.teams?.some((team) => + userScopeUUIDs.includes(team.teamId) + ) + })) + .toSorted(ownerSorter) + .map(rowDecorator) + + return rows +} + +export { buildUtilitiesTableData } diff --git a/src/server/utilities/transformers/utility-to-entity-row.js b/src/server/utilities/transformers/utility-to-entity-row.js index 1d098771..c800f488 100644 --- a/src/server/utilities/transformers/utility-to-entity-row.js +++ b/src/server/utilities/transformers/utility-to-entity-row.js @@ -1,35 +1,76 @@ import { removeUrlParts } from '~/src/server/common/helpers/remove-url-parts.js' +import { + renderComponent, + renderIcon +} from '~/src/server/common/helpers/nunjucks/render-component.js' -function utilityToEntityRow(utilityType) { - return (utility) => [ - { - kind: 'link', - value: utility.id, - url: `/utilities/${utilityType}/${utility.id}` - }, - { - kind: 'group', - value: utility?.teams?.map((team) => ({ - kind: 'link', - value: team.name, - url: `/teams/${team.teamId}` - })) - }, - { - kind: 'text', - value: utility.primaryLanguage - }, - { - kind: 'link', - value: removeUrlParts(utility.url), - url: utility.url, - newWindow: true - }, - { - kind: 'date', - value: utility.createdAt +function utilityToEntityRow(utilityType, isAuthenticated) { + return (utility) => { + const icon = utility.isOwner + ? renderComponent( + 'tool-tip', + { text: 'Owned Utility', classes: 'app-tool-tip--small' }, + [renderIcon('star-icon', { classes: 'app-icon--tiny' })] + ) + : '' + + return { + cells: [ + ...(isAuthenticated + ? [ + { + headers: 'owner', + isCentered: true, + classes: 'app-entity-table__cell--owned', + entity: { kind: 'html', value: icon } + } + ] + : []), + { + headers: 'service', + entity: { + kind: 'link', + value: utility.id, + url: `/utilities/${utilityType}/${utility.id}` + } + }, + { + headers: 'service', + entity: { + kind: 'group', + value: utility?.teams?.map((team) => ({ + kind: 'link', + value: team.name, + url: `/teams/${team.teamId}` + })) + } + }, + { + headers: 'service', + entity: { + kind: 'text', + value: utility.primaryLanguage + } + }, + { + headers: 'service', + entity: { + kind: 'link', + value: removeUrlParts(utility.url), + url: utility.url, + newWindow: true + } + }, + { + headers: 'service', + entity: { + kind: 'date', + value: utility.createdAt + } + } + ] } - ] + } } export { utilityToEntityRow } diff --git a/src/server/utilities/transformers/utility-to-entity-row.test.js b/src/server/utilities/transformers/utility-to-entity-row.test.js index a464b1c6..599fe215 100644 --- a/src/server/utilities/transformers/utility-to-entity-row.test.js +++ b/src/server/utilities/transformers/utility-to-entity-row.test.js @@ -1,43 +1,129 @@ -import { config } from '~/src/config/config.js' import { utilityToEntityRow } from '~/src/server/utilities/transformers/utility-to-entity-row.js' import { templatesFixture } from '~/src/__fixtures__/templates.js' -const githubOrg = config.get('githubOrg') - describe('#utilityToEntityRow', () => { - test('Should provide expected "templates" transformation', () => { - expect( - utilityToEntityRow('templates')(templatesFixture.templates.at(0)) - ).toEqual([ - { - kind: 'link', - url: '/utilities/templates/cdp-node-frontend-template', - value: 'cdp-node-frontend-template' - }, - { - kind: 'group', - value: [ - { - kind: 'link', - url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', - value: 'Platform' + describe('When authenticated', () => { + test('Should provide expected "templates" transformation', () => { + expect( + utilityToEntityRow( + 'templates', + true + )(templatesFixture.repositories.at(0)) + ).toEqual({ + cells: [ + { + classes: 'app-entity-table__cell--owned', + entity: { + kind: 'html', + value: '' + }, + headers: 'owner', + isCentered: true + }, + { + entity: { + kind: 'link', + url: '/utilities/templates/cdp-dotnet-backend-template', + value: 'cdp-dotnet-backend-template' + }, + headers: 'service' + }, + { + entity: { + kind: 'group', + value: [ + { + kind: 'link', + url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', + value: 'Platform' + } + ] + }, + headers: 'service' + }, + { + entity: { + kind: 'text', + value: 'C#' + }, + headers: 'service' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-dotnet-backend-template', + value: 'DEFRA/cdp-dotnet-backend-template' + }, + headers: 'service' + }, + { + entity: { + kind: 'date', + value: '2023-08-24T07:08:56+00:00' + }, + headers: 'service' + } + ] + }) + }) + }) + + describe('When not authenticated', () => { + test('Should provide expected "templates" transformation', () => { + expect( + utilityToEntityRow( + 'templates', + false + )(templatesFixture.repositories.at(0)) + ).toEqual({ + cells: [ + { + entity: { + kind: 'link', + url: '/utilities/templates/cdp-dotnet-backend-template', + value: 'cdp-dotnet-backend-template' + }, + headers: 'service' + }, + { + entity: { + kind: 'group', + value: [ + { + kind: 'link', + url: '/teams/aabe63e7-87ef-4beb-a596-c810631fc474', + value: 'Platform' + } + ] + }, + headers: 'service' + }, + { + entity: { + kind: 'text', + value: 'C#' + }, + headers: 'service' + }, + { + entity: { + kind: 'link', + newWindow: true, + url: 'https://github.com/DEFRA/cdp-dotnet-backend-template', + value: 'DEFRA/cdp-dotnet-backend-template' + }, + headers: 'service' + }, + { + entity: { + kind: 'date', + value: '2023-08-24T07:08:56+00:00' + }, + headers: 'service' } ] - }, - { - kind: 'text', - value: 'JavaScript' - }, - { - kind: 'link', - newWindow: true, - url: `https://github.com/${githubOrg}/cdp-node-frontend-template`, - value: `${githubOrg}/cdp-node-frontend-template` - }, - { - kind: 'date', - value: '2023-04-26T15:27:09+00:00' - } - ]) + }) + }) }) }) diff --git a/src/server/utilities/views/list.njk b/src/server/utilities/views/list.njk index d4d52edf..bae6e688 100644 --- a/src/server/utilities/views/list.njk +++ b/src/server/utilities/views/list.njk @@ -3,20 +3,12 @@ {% block content %} {% call appSplitPane() %} {{ appPageHeading({ - text: heading + text: pageHeading.text, + intro: pageHeading.intro }) }} - {{ appEntityList({ - headings: [ - { text: "Item", size: "large" }, - { text: "Team", size: "medium" }, - { text: "Language", size: "small" }, - { text: "GitHub Repository", size: "massive" }, - { text: "Created", size: "medium" } - ], - entityRows: entityRows, - noResult: noResult - }) }} + {{ appEntityTable(tableData) }} + {% endcall %} {% endblock %}