From c8ac094b6a4e423019bfcc6cc95dca5af8faed01 Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:38:18 -0700 Subject: [PATCH 01/15] scroll to top, fix redirect for gateway detail * add return null * scroll to top of details on nav from /list --- .../components/no-gateway-redirect/no-gateway-redirect.tsx | 1 + src/nextapp/pages/manager/gateways/detail.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx index 414fdb3db..a9f45fcd6 100644 --- a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx +++ b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx @@ -12,6 +12,7 @@ const NoGatewayRedirect = () => { router.push('/manager/gateways/list'); } }, [hasNamespace]); + return null }; export default NoGatewayRedirect; diff --git a/src/nextapp/pages/manager/gateways/detail.tsx b/src/nextapp/pages/manager/gateways/detail.tsx index 1bf000e92..4a93ca708 100644 --- a/src/nextapp/pages/manager/gateways/detail.tsx +++ b/src/nextapp/pages/manager/gateways/detail.tsx @@ -158,6 +158,9 @@ const NamespacesPage: React.FC = () => { text: 'Your Organization and Business Unit will appear here', }; }, [namespace]); + useEffect(() => { + window.scrollTo(0, 0); + }, []); const handleDelete = React.useCallback(async () => { if (user?.namespace) { toast({ From 539068539e79668fe098d2168418569639bdd778 Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:20:40 -0700 Subject: [PATCH 02/15] Cypress/ns to gw cli tests (#1158) Cypress tests for new features in `gwa` CLI from NS to GW. --- e2e/Dockerfile | 2 +- e2e/cypress/pageObjects/products.ts | 10 +- .../tests/16-gwa-cli/01-cli-commands.ts | 19 +-- .../02-cli-generate-config-quick-start.ts | 115 +++++++++++++++ ... => 03-cli-generate-config-client-cred.ts} | 41 ++++-- .../tests/16-gwa-cli/04-cli-gateway-create.ts | 131 ++++++++++++++++++ e2e/cypress/tests/20-gateways/01-list.ts | 2 - e2e/test-data-dependencies.md | 4 +- 8 files changed, 284 insertions(+), 40 deletions(-) create mode 100644 e2e/cypress/tests/16-gwa-cli/02-cli-generate-config-quick-start.ts rename e2e/cypress/tests/16-gwa-cli/{02-cli-generate-config.ts => 03-cli-generate-config-client-cred.ts} (68%) create mode 100644 e2e/cypress/tests/16-gwa-cli/04-cli-gateway-create.ts diff --git a/e2e/Dockerfile b/e2e/Dockerfile index f5b7aa069..2a093d736 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -22,7 +22,7 @@ COPY e2e/*.yml /e2e COPY e2e/entrypoint.sh /tmp ADD e2e/cypress /e2e/cypress -RUN curl -v -L -O https://github.com/bcgov/gwa-cli/releases/download/v3.0.0/gwa_Linux_x86_64.tgz \ +RUN curl -v -L -O https://github.com/bcgov/gwa-cli/releases/download/v3.0.4/gwa_Linux_x86_64.tgz \ && tar -xzf gwa_Linux_x86_64.tgz \ && mv gwa /usr/local/bin/. diff --git a/e2e/cypress/pageObjects/products.ts b/e2e/cypress/pageObjects/products.ts index e1b1ff727..eab3c1d57 100644 --- a/e2e/cypress/pageObjects/products.ts +++ b/e2e/cypress/pageObjects/products.ts @@ -2,6 +2,7 @@ import { updateYamlDocument } from "@atomist/yaml-updater"; import _ = require("cypress/types/lodash"); const YAML = require('yamljs'); +const { kebabCase } = require('lodash'); class Products { path: string = '/manager/products' @@ -57,7 +58,7 @@ class Products { } editProduct(productName: string) { - const pname: string = productName.toLowerCase().replaceAll(' ', '-') + const pname: string = kebabCase(productName) cy.get(`[data-testid=${pname}-more-options-btn]`).first().click() cy.get(`[data-testid=${pname}-edit-btn]`).first().click() // cy.get(this.updateBtn).click() @@ -92,7 +93,7 @@ class Products { } editProductEnvironment(productName: string, envName: string) { - const pname: string = productName.toLowerCase().replaceAll(' ', '-') + const pname: string = kebabCase(productName) let env = this.getTestIdEnvName(envName); cy.get(`[data-testid=${pname}-${env}-edit-btn]`).click() cy.wait(2000) @@ -228,7 +229,7 @@ class Products { } deleteProductEnvironment(productName: string, envName: string) { - const pname: string = productName.toLowerCase().replaceAll(' ', '-') + const pname: string = kebabCase(productName) let env = this.getTestIdEnvName(envName); cy.get(`[data-testid=${pname}-${env}-more-options-btn]`).click() cy.get(`[data-testid=${pname}-${env}-delete-btn]`).click() @@ -236,8 +237,7 @@ class Products { } deleteProduct(productName: string) { - // this.editProduct(productName) - const pname: string = productName.toLowerCase().replaceAll(' ', '-') + const pname: string = kebabCase(productName) cy.get(`[data-testid=${pname}-edit-btn]`).first().click({ force: true }) cy.get(`[data-testid=${pname}-delete-btn]`).first().click({ force: true }) cy.get(this.deleteProductConfirmationBtn).click() diff --git a/e2e/cypress/tests/16-gwa-cli/01-cli-commands.ts b/e2e/cypress/tests/16-gwa-cli/01-cli-commands.ts index 6112a17a2..0b9b3d869 100644 --- a/e2e/cypress/tests/16-gwa-cli/01-cli-commands.ts +++ b/e2e/cypress/tests/16-gwa-cli/01-cli-commands.ts @@ -1,22 +1,11 @@ -import LoginPage from '../../pageObjects/login' -import ApplicationPage from '../../pageObjects/applications' -import ApiDirectoryPage from '../../pageObjects/apiDirectory' -import MyAccessPage from '../../pageObjects/myAccess' -const YAML = require('yamljs'); -let userSession: any let cli = require("../../fixtures/test_data/gwa-cli.json") +let userSession: any var cleanedUrl = Cypress.env('BASE_URL').replace(/^http?:\/\//i, ""); -const jose = require('node-jose') -describe('Verify CLI commands', () => { - const login = new LoginPage() - const apiDir = new ApiDirectoryPage() - const app = new ApplicationPage() - const ma = new MyAccessPage() +describe('Verify config / login CLI commands', () => { let namespace: string before(() => { - // cy.visit('/') cy.deleteAllCookies() cy.reload(true) }) @@ -25,7 +14,6 @@ describe('Verify CLI commands', () => { cy.preserveCookies() cy.fixture('apiowner').as('apiowner') cy.fixture('common-testdata').as('common-testdata') - // cy.visit(login.path) }) it('authenticates Janis (api owner) to get the user session token', () => { @@ -90,7 +78,6 @@ describe('Verify CLI commands', () => { }); }) - it('Check gwa gateway list command and verify the created namespace in the list', () => { cy.executeCliCommand('gwa gateway list --host ' + cleanedUrl + ' --scheme http').then((response) => { expect(response.stdout).to.contain(namespace); @@ -103,4 +90,4 @@ describe('Verify CLI commands', () => { cy.deleteAllCookies() }) -}) \ No newline at end of file +}) diff --git a/e2e/cypress/tests/16-gwa-cli/02-cli-generate-config-quick-start.ts b/e2e/cypress/tests/16-gwa-cli/02-cli-generate-config-quick-start.ts new file mode 100644 index 000000000..403087a01 --- /dev/null +++ b/e2e/cypress/tests/16-gwa-cli/02-cli-generate-config-quick-start.ts @@ -0,0 +1,115 @@ +import LoginPage from '../../pageObjects/login' +import ApplicationPage from '../../pageObjects/applications' +import ApiDirectoryPage from '../../pageObjects/apiDirectory' +import MyAccessPage from '../../pageObjects/myAccess' +import HomePage from '../../pageObjects/home'; +import Products from '../../pageObjects/products'; +const YAML = require('yamljs'); +let cli = require("../../fixtures/test_data/gwa-cli.json") +const { v4: uuidv4 } = require('uuid') +const jose = require('node-jose') + +let userSession: any +const customId = uuidv4().replace(/-/g, '').toLowerCase().substring(0, 3) +const serviceName = 'my-service-' + customId + +describe('Verify CLI commands for generate/apply config', () => { + const login = new LoginPage() + const apiDir = new ApiDirectoryPage() + const app = new ApplicationPage() + const ma = new MyAccessPage() + const pd = new Products() + let namespace: string + const home = new HomePage() + + before(() => { + // cy.visit('/') + cy.reload(true) + }) + + beforeEach(() => { + cy.preserveCookies() + cy.fixture('apiowner').as('apiowner') + // cy.visit(login.path) + }) + + it('authenticates Janis (api owner) to get the user session token', () => { + cy.get('@apiowner').then(({ apiTest }: any) => { + cy.getUserSessionTokenValue(apiTest.namespace, false).then((value) => { + userSession = value + }) + }) + }) + + it('Check gwa config command to set token', () => { + cy.executeCliCommand('gwa config set --token ' + userSession).then((response) => { + expect(response.stdout).to.contain("Config settings saved") + }); + }) + + it('Check gwa command to generate config for client credential template', () => { + const command = [ + 'gwa generate-config --template quick-start', + `--service ${serviceName}`, + '--upstream https://httpbin.org', + '--org ministry-of-health', + '--org-unit planning-and-innovation-division', + '--out gw-config-qs.yaml' + ].join(' '); + cy.executeCliCommand(command).then((response) => { + expect(response.stdout).to.contain("File gw-config-qs.yaml created") + }); + }) + + it('Check gwa command to apply generated config', () => { + cy.executeCliCommand('gwa apply -i gw-config-qs.yaml').then((response) => { + expect(response.stdout).to.contain("3/3 Published, 0 Skipped") + let wordOccurrences = (response.stdout.match(/\bcreated\b/g) || []).length; + expect(wordOccurrences).to.equal(2) + }); + }) + + it('Check gwa status --hosts include routes', () => { + cy.executeCliCommand('gwa status --hosts').then((response) => { + expect(response.stdout).to.contain('https://' + serviceName + '.dev.api.gov.bc.ca') + }); + }) + + it('activates namespace in Portal', () => { + cy.executeCliCommand('gwa gateway current').then((response) => { + const namespace = response.stdout.match(/\bgw-\w+/g)[0] + cy.activateGateway(namespace) + }) + }) + + it('Verify that the product created through gwa command is displayed in the portal', () => { + cy.visit(pd.path) + pd.editProductEnvironment(serviceName + ' API', 'dev') + }) + + it('Verify that the dataset created through GWA comand is assocuated with the product', () => { + cy.visit(pd.path) + pd.verifyDataset(serviceName, serviceName + ' API') + }) + + it('Verify service name validation error in new namespace', () => { + const command = [ + 'gwa generate-config --template quick-start', + `--service ${serviceName}`, + '--upstream https://httpbin.org', + '--org ministry-of-health', + '--org-unit planning-and-innovation-division' + ].join(' '); + cy.executeCliCommand('gwa gateway create --generate').then((response) => { + const namespace = response.stdout.match(/\bgw-\w+/g)[0] + cy.executeCliCommand(command).then((response) => { + expect(response.stderr).to.contain(`Error: Service ${serviceName} is already in use. Suggestion: ${namespace}-${serviceName}`) + }); + }); + }) + + after(() => { + cy.logout() +}) + +}) \ No newline at end of file diff --git a/e2e/cypress/tests/16-gwa-cli/02-cli-generate-config.ts b/e2e/cypress/tests/16-gwa-cli/03-cli-generate-config-client-cred.ts similarity index 68% rename from e2e/cypress/tests/16-gwa-cli/02-cli-generate-config.ts rename to e2e/cypress/tests/16-gwa-cli/03-cli-generate-config-client-cred.ts index e090a8ad6..fa0e28413 100644 --- a/e2e/cypress/tests/16-gwa-cli/02-cli-generate-config.ts +++ b/e2e/cypress/tests/16-gwa-cli/03-cli-generate-config-client-cred.ts @@ -5,18 +5,21 @@ import MyAccessPage from '../../pageObjects/myAccess' import HomePage from '../../pageObjects/home'; import Products from '../../pageObjects/products'; const YAML = require('yamljs'); -let userSession: any let cli = require("../../fixtures/test_data/gwa-cli.json") - +const { v4: uuidv4 } = require('uuid') const jose = require('node-jose') +let userSession: any +let namespace: string +const customId = uuidv4().replace(/-/g, '').toLowerCase().substring(0, 3) +const serviceName = 'my-service-' + customId + describe('Verify CLI commands for generate/apply config', () => { const login = new LoginPage() const apiDir = new ApiDirectoryPage() const app = new ApplicationPage() const ma = new MyAccessPage() const pd = new Products() - let namespace: string const home = new HomePage() before(() => { @@ -45,26 +48,38 @@ describe('Verify CLI commands for generate/apply config', () => { }) it('Check gwa command to generate config for client credential template', () => { - cy.executeCliCommand('gwa generate-config --template client-credentials-shared-idp --service my-service --upstream https://httpbin.org --org ministry-of-health --org-unit planning-and-innovation-division --out gw-config.yaml').then((response) => { - expect(response.stdout).to.contain("File gw-config.yaml created") + const serviceName = 'my-service-' + customId + const command = [ + 'gwa generate-config --template client-credentials-shared-idp', + `--service ${serviceName}`, + '--upstream https://httpbin.org', + '--org ministry-of-health', + '--org-unit planning-and-innovation-division', + '--out gw-config-cc.yaml' + ].join(' '); + cy.executeCliCommand(command).then((response) => { + expect(response.stdout).to.contain("File gw-config-cc.yaml created") }); }) it('Check gwa command to apply generated config', () => { - cy.executeCliCommand('gwa apply -i gw-config.yaml').then((response) => { + cy.executeCliCommand('gwa apply -i gw-config-cc.yaml').then((response) => { + expect(response.stdout).to.contain("4/4 Published, 0 Skipped") let wordOccurrences = (response.stdout.match(/\bcreated\b/g) || []).length; expect(wordOccurrences).to.equal(3) - namespace = response.stdout.match(/\bgw-\w+/g)[0] }); }) - it('activates new namespace', () => { - cy.activateGateway(namespace) + it('activates namespace in Portal', () => { + cy.executeCliCommand('gwa gateway current').then((response) => { + namespace = response.stdout.match(/\bgw-\w+/g)[0] + cy.activateGateway(namespace) + }) }) it('Verify that the product created through gwa command is displayed in the portal', () => { cy.visit(pd.path) - pd.editProductEnvironment('my-service API', 'dev') + pd.editProductEnvironment(serviceName + ' API', 'dev') }) it('Verify the Authorization scope and issuer details for the product', () => { @@ -77,11 +92,7 @@ describe('Verify CLI commands for generate/apply config', () => { it('Verify that the dataset created through GWA comand is assocuated with the product', () => { cy.visit(pd.path) - pd.verifyDataset('my-service', 'my-service API') - }) - - it('Navigate to home path', () => { - cy.visit(login.path) + pd.verifyDataset(serviceName, serviceName + ' API') }) after(() => { diff --git a/e2e/cypress/tests/16-gwa-cli/04-cli-gateway-create.ts b/e2e/cypress/tests/16-gwa-cli/04-cli-gateway-create.ts new file mode 100644 index 000000000..f23b355c6 --- /dev/null +++ b/e2e/cypress/tests/16-gwa-cli/04-cli-gateway-create.ts @@ -0,0 +1,131 @@ +import NameSpacePage from '../../pageObjects/namespace' + +const { v4: uuidv4 } = require('uuid') +let cli = require("../../fixtures/test_data/gwa-cli.json") + +let userSession: any +let gatewayId: string +let displayName: string +let customId: string + +var cleanedUrl = Cypress.env('BASE_URL').replace(/^http?:\/\//i, ""); + +describe('Verify "gateway create" and "gateway list" commands', () => { + const ns = new NameSpacePage() + customId = uuidv4().replace(/-/g, '').toLowerCase().substring(0, 3) + + before(() => { + cy.deleteAllCookies() + cy.reload(true) + }) + + beforeEach(() => { + cy.preserveCookies() + cy.fixture('apiowner').as('apiowner') + cy.fixture('common-testdata').as('common-testdata') + }) + + it('authenticates Janis (api owner) to get the user session token', () => { + cy.getUserSessionTokenValue('', false).then((value) => { + userSession = value + }) + }) + + it('Set token with gwa config command', () => { + cy.exec('gwa config set --token ' + userSession, { timeout: 3000, failOnNonZeroExit: false }).then((response) => { + expect(response.stdout).to.contain("Config settings saved") + }); + }) + + it('create gateway - id and display name provided', () => { + cy.get('@common-testdata').then(({ myGateways }: any) => { + gatewayId = myGateways["namespace1"].gatewayId + '-' + customId + displayName = myGateways["namespace1"].displayName + cy.executeCliCommand('gwa gateway create --gateway-id ' + gatewayId + ' --display-name ' + displayName + ' --host ' + cleanedUrl + ' --scheme http').then((response) => { + assert.isNotNaN(response.stdout) + const gatewayIdMatch = response.stdout.match(/Gateway ID: ([\w-]+)/); + if (gatewayIdMatch && gatewayIdMatch[1]) { + const gatewayIdResult = gatewayIdMatch[1]; + assert.equal(gatewayIdResult, gatewayId); + } else { + throw new Error('Failed to extract Gateway ID from response: ' + response.stdout); + } + const displayNameMatch = response.stdout.match(/display name: ([\w-]+)/); + if (displayNameMatch && displayNameMatch[1]) { + const displayNameResult = displayNameMatch[1]; + assert.equal(displayNameResult, displayName); + } else { + throw new Error('Failed to extract Display Name from response: ' + response.stdout); + } + }); + // Verify the created gateway in Portal + cy.visit(ns.listPath) + cy.get(`[data-testid="ns-list-item-${gatewayId}"]`) + .should('contain.text', gatewayId) + .should('contain.text', displayName) + }); + }); + + it('verify gateway list command', () => { + cy.executeCliCommand('gwa gateway list').then((response) => { + assert.isNotNaN(response.stdout) + assert.isTrue(response.stdout.includes(gatewayId), `The output should include the gateway ID: ${gatewayId}`) + assert.isTrue(response.stdout.includes(displayName), `The output should include the display name: ${displayName}`) + }); + cy.deleteGatewayCli(gatewayId, false) + }); + + it('create gateway - id provided', () => { + cy.get('@common-testdata').then(({ myGateways }: any) => { + gatewayId = myGateways["namespace2"].gatewayId + '-' + customId + displayName = "janis's Gateway" + cy.executeCliCommand('gwa gateway create --gateway-id ' + gatewayId + ' --host ' + cleanedUrl + ' --scheme http').then((response) => { + assert.isNotNaN(response.stdout) + const gatewayIdMatch = response.stdout.match(/Gateway ID: ([\w-]+)/); + if (gatewayIdMatch && gatewayIdMatch[1]) { + const gatewayIdResult = gatewayIdMatch[1]; + assert.equal(gatewayIdResult, gatewayId); + } else { + throw new Error('Failed to extract Gateway ID from response: ' + response.stdout); + } + }); + // Verify the created gateway + cy.visit(ns.listPath) + cy.get(`[data-testid="ns-list-item-${gatewayId}"]`) + .should('contain.text', gatewayId) + .should('contain.text', displayName) + cy.deleteGatewayCli(gatewayId, false) + }); + }); + + it('create gateway - display name provided', () => { + cy.get('@common-testdata').then(({ myGateways }: any) => { + let generatedGatewayId: string + displayName = myGateways["namespace3"].displayName + cy.executeCliCommand('gwa gateway create --display-name ' + displayName + ' --host ' + cleanedUrl + ' --scheme http').then((response) => { + assert.isNotNaN(response.stdout) + const gatewayIdMatch = response.stdout.match(/Gateway ID: ([\w-]+)/); + generatedGatewayId = gatewayIdMatch[1]; + const displayNameMatch = response.stdout.match(/display name: ([\w-]+)/); + if (displayNameMatch && displayNameMatch[1]) { + const displayNameResult = displayNameMatch[1]; + assert.equal(displayNameResult, displayName); + } else { + throw new Error('Failed to extract Display Name from response: ' + response.stdout); + } + // Verify the created gateway + cy.visit(ns.listPath) + cy.get(`[data-testid="ns-list-item-${generatedGatewayId}"]`) + .should('contain.text', generatedGatewayId) + .should('contain.text', displayName) + cy.deleteGatewayCli(generatedGatewayId, false) + }); + }); + }); + + after(() => { + cy.logout() + cy.clearLocalStorage({ log: true }) + cy.deleteAllCookies() + }) +}) diff --git a/e2e/cypress/tests/20-gateways/01-list.ts b/e2e/cypress/tests/20-gateways/01-list.ts index e95963d85..c31ad87f1 100644 --- a/e2e/cypress/tests/20-gateways/01-list.ts +++ b/e2e/cypress/tests/20-gateways/01-list.ts @@ -1,4 +1,3 @@ -import { report } from 'process' import NameSpacePage from '../../pageObjects/namespace' let gateways: any @@ -34,7 +33,6 @@ describe('My Gateways list page', () => { it('create a set of namespaces', () => { cy.get('@common-testdata').then(({ myGateways }: any) => { gateways = myGateways - gateways["namespace1"] = gateways["namespace1"] Cypress._.forEach(gateways, (gateway) => { cy.createGateway(gateway.gatewayId + '-' + customId, gateway.displayName); }); diff --git a/e2e/test-data-dependencies.md b/e2e/test-data-dependencies.md index 06314847e..519cb98e1 100644 --- a/e2e/test-data-dependencies.md +++ b/e2e/test-data-dependencies.md @@ -102,7 +102,9 @@ | │   08-namespaces.cy.ts | 1.1 and 15.1 | | 16-gwa-cli | | | │   01-cli-commands.ts | NA | -| │   02-cli-generate-config.ts | 16.1 | +| │   02-cli-generate-config-quick-start.ts | 16.1 | +| │   03-cli-generate-config-client-cred.ts | 1.1, 2.1 to 2.3, 15.1, 15.5. 16.1 | +| │   04-cli-gateway-create.ts | NA | | 17-delete-application | | | │   01-delete-application-without-access.cy.ts | NA | | │   02-delete-application-with-pending-request.cy.ts | 17.1 (CONFIRM: requires product, created in 01-api-key?) | From 43994fa43afee423ef2d5c7dc3bc572f90b6518c Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:28:38 -0700 Subject: [PATCH 03/15] replace Namespace with Gateway in org permission (#1159) --- .../components/namespace-access/organization-groups-access.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/components/namespace-access/organization-groups-access.tsx b/src/nextapp/components/namespace-access/organization-groups-access.tsx index 61eef69b0..84c537e15 100644 --- a/src/nextapp/components/namespace-access/organization-groups-access.tsx +++ b/src/nextapp/components/namespace-access/organization-groups-access.tsx @@ -136,7 +136,7 @@ const OrganizationGroupsAccess: React.FC = ({ {d.scopes.map((t) => ( - {t} + {t.replace(/Namespace/g, 'Gateway')} ))} From ba687c18fbea7b43575019307e734cc248225dd4 Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:22:02 -0700 Subject: [PATCH 04/15] Feature/ns to gw redirect (#1162) * fix some capitlization * fix no gw redirect handling and add toast --- e2e/cypress/tests/20-gateways/01-list.ts | 6 +++ .../gateway-toast-handler.tsx | 34 +++++++++++++++ .../no-gateway-redirect.tsx | 41 +++++++++++++++---- src/nextapp/pages/_app.tsx | 2 + src/nextapp/pages/manager/gateways/list.tsx | 12 +++--- 5 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx diff --git a/e2e/cypress/tests/20-gateways/01-list.ts b/e2e/cypress/tests/20-gateways/01-list.ts index c31ad87f1..6e7f5eb3b 100644 --- a/e2e/cypress/tests/20-gateways/01-list.ts +++ b/e2e/cypress/tests/20-gateways/01-list.ts @@ -47,6 +47,12 @@ describe('My Gateways list page', () => { }); }) + it('Verify redirect to My Gateways page if no gateway selected', () => { + cy.visit(ns.detailPath) + cy.wait(2000) + cy.verifyToastMessage('First select a Gateway to view that page') + }) + it('Check Gateway link goes to details page', () => { cy.visit(ns.listPath) cy.get(`[data-testid="ns-list-activate-link-${gateways["namespace1"].gatewayId + '-' + customId}"]`).click() diff --git a/src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx b/src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx new file mode 100644 index 000000000..b67ca18e5 --- /dev/null +++ b/src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { useToast } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; + +const GatewayToastHandler = () => { + const toast = useToast(); + const router = useRouter(); + + React.useEffect(() => { + const handleRouteChange = () => { + const showToast = localStorage.getItem('showNoGatewayToast'); + if (showToast === 'true') { + toast.closeAll(); + toast({ + title: `First select a Gateway to view that page`, + status: 'error', + isClosable: true, + duration: 5000, + }); + localStorage.removeItem('showNoGatewayToast'); + } + }; + + router.events.on('routeChangeComplete', handleRouteChange); + + return () => { + router.events.off('routeChangeComplete', handleRouteChange); + }; + }, [toast, router]); + + return null; +}; + +export default GatewayToastHandler; \ No newline at end of file diff --git a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx index a9f45fcd6..0407a76c7 100644 --- a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx +++ b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx @@ -5,14 +5,41 @@ import { useAuth } from '@/shared/services/auth'; const NoGatewayRedirect = () => { const router = useRouter(); const { user } = useAuth(); - const hasNamespace = !!user?.namespace; + const [isChecking, setIsChecking] = React.useState(true); React.useEffect(() => { - if (!hasNamespace) { - router.push('/manager/gateways/list'); - } - }, [hasNamespace]); - return null + const checkNamespaceAndRedirect = async () => { + // Wait for a short period to ensure user data is loaded + await new Promise(resolve => setTimeout(resolve, 2000)); + + setIsChecking(false); + + if (!user?.namespace) { + try { + // Set localStorage item to show toast + await new Promise((resolve, reject) => { + try { + localStorage.setItem('showNoGatewayToast', 'true'); + resolve(); + } catch (error) { + reject(error); + } + }); + await router.push('/manager/gateways/list'); + } catch (error) { + console.error('Error during redirect process:', error); + } + } + }; + + checkNamespaceAndRedirect(); + }, [user, router]); + + if (isChecking) { + return null; // could return a loading indicator + } + + return null; }; -export default NoGatewayRedirect; +export default NoGatewayRedirect; \ No newline at end of file diff --git a/src/nextapp/pages/_app.tsx b/src/nextapp/pages/_app.tsx index 6b06ad3b1..7cf5709b3 100644 --- a/src/nextapp/pages/_app.tsx +++ b/src/nextapp/pages/_app.tsx @@ -31,6 +31,7 @@ import '@/shared/styles/global.css'; import { AppWrapper } from './context'; import '../../mocks'; import CompleteProfile from '@/components/complete-profile'; +import GatewayToastHandler from '@/components/no-gateway-redirect/gateway-toast-handler'; const footerItems = [ { href: 'http://www2.gov.bc.ca/gov/content/home', text: 'Home' }, @@ -122,6 +123,7 @@ const App: React.FC = ({ Component, pageProps }) => { }} > + diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index b0a6246a8..6e9389835 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -106,7 +106,7 @@ const MyGatewaysPage: React.FC = () => { const handleNamespaceChange = React.useCallback( (namespace: Namespace) => async () => { toast({ - title: `Switching to gateway: ${namespace.displayName}`, + title: `Switching to Gateway: ${namespace.displayName}`, status: 'info', isClosable: true, }); @@ -116,7 +116,7 @@ const MyGatewaysPage: React.FC = () => { toast.closeAll(); client.invalidateQueries(); toast({ - title: `Switched to gateway: ${namespace.displayName}`, + title: `Switched to Gateway: ${namespace.displayName}`, status: 'success', isClosable: true, }); @@ -124,7 +124,7 @@ const MyGatewaysPage: React.FC = () => { } catch (err) { toast.closeAll(); toast({ - title: 'Unable to switch gateways', + title: 'Unable to switch Gateways', status: 'error', isClosable: true, }); @@ -264,11 +264,11 @@ const MyGatewaysPage: React.FC = () => { {isSuccess && (namespaceSearchResults.length === 1 ? ( - {namespaceSearchResults.length} gateway + {namespaceSearchResults.length} Gateway ) : ( - {namespaceSearchResults.length} gateways + {namespaceSearchResults.length} Gateways ))} - {isLoading && Loading gateways...} + {isLoading && Loading Gateways...} {isError && Gateways failed to load} {isSuccess && ( <> From 7355065abbeca053446cd44777d25cc3cae72b0c Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Tue, 10 Sep 2024 12:24:39 -0700 Subject: [PATCH 05/15] more details on login --- .../components/gateway-get-started/gateway-get-started.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx index 6e50203df..973ff5a64 100644 --- a/src/nextapp/components/gateway-get-started/gateway-get-started.tsx +++ b/src/nextapp/components/gateway-get-started/gateway-get-started.tsx @@ -312,7 +312,7 @@ const GatewayGetStarted: React.FC = ({ )} Date: Fri, 3 May 2024 09:01:07 -0700 Subject: [PATCH 06/15] show error for invalid display name format --- .../edit-display-name/edit-display-name.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/nextapp/components/edit-display-name/edit-display-name.tsx b/src/nextapp/components/edit-display-name/edit-display-name.tsx index 07dd2021c..28a859585 100644 --- a/src/nextapp/components/edit-display-name/edit-display-name.tsx +++ b/src/nextapp/components/edit-display-name/edit-display-name.tsx @@ -42,18 +42,24 @@ const EditNamespaceDisplayName: React.FC = ({ ); const minCharLimit = 3; const maxCharLimit = 30; + const [isValidFormat, setIsValidFormat] = React.useState(true); + const validNameRegex = /^[a-zA-Z0-9][\w\s().'/-]*$/; + const handleInputChange = (event) => { const { value } = event.target; setInputValue(value); setCharCount(value.length); + setIsValidFormat(validNameRegex.test(value)); }; + const form = React.useRef(); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); - if (charCount >= minCharLimit && charCount <= maxCharLimit) { + if (charCount >= minCharLimit && charCount <= maxCharLimit && isValidFormat) { updateNamespaceDisplayName(); } }; + const updateNamespaceDisplayName = async () => { if (form.current) { try { @@ -79,9 +85,13 @@ const EditNamespaceDisplayName: React.FC = ({ } } }; + const handleSaveClick = () => { form.current?.requestSubmit(); }; + + const isInputValid = charCount >= minCharLimit && charCount <= maxCharLimit && isValidFormat; + return ( <> From 70a962714a1c7d8cefa41caee690487c5c618649 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Thu, 12 Sep 2024 11:09:06 -0700 Subject: [PATCH 07/15] reset edit display name input on cancel --- .../edit-display-name/edit-display-name.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/nextapp/components/edit-display-name/edit-display-name.tsx b/src/nextapp/components/edit-display-name/edit-display-name.tsx index 28a859585..a84bc6306 100644 --- a/src/nextapp/components/edit-display-name/edit-display-name.tsx +++ b/src/nextapp/components/edit-display-name/edit-display-name.tsx @@ -33,7 +33,7 @@ const EditNamespaceDisplayName: React.FC = ({ queryKey, }) => { const toast = useToast(); - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, onOpen, onClose: originalOnClose } = useDisclosure(); const queryClient = useQueryClient(); const mutate = useApiMutation(mutation); const [inputValue, setInputValue] = React.useState(data.displayName || ''); @@ -68,7 +68,7 @@ const EditNamespaceDisplayName: React.FC = ({ const entries = Object.fromEntries(formData); await mutate.mutateAsync(entries); queryClient.invalidateQueries(queryKey); - onClose(); + originalOnClose(); toast({ title: 'Display name successfully edited', status: 'success', @@ -92,6 +92,14 @@ const EditNamespaceDisplayName: React.FC = ({ const isInputValid = charCount >= minCharLimit && charCount <= maxCharLimit && isValidFormat; + // Add this new function to handle closing and resetting + const handleClose = () => { + setInputValue(data.displayName || ''); + setCharCount(data.displayName?.length || 0); + setIsValidFormat(true); + originalOnClose(); + }; + return ( <> - + Edit display name @@ -154,7 +162,7 @@ const EditNamespaceDisplayName: React.FC = ({ diff --git a/src/nextapp/shared/theme.ts b/src/nextapp/shared/theme.ts index a73d8317b..b09e7e946 100644 --- a/src/nextapp/shared/theme.ts +++ b/src/nextapp/shared/theme.ts @@ -58,7 +58,7 @@ const _focus = { boxShadow: 'sm', }; const _disabled = { - opacity: 0.3, + opacity: 0.4, cursor: 'not-allowed', }; const _invalid = { @@ -122,7 +122,8 @@ const buttonVariants = { _disabled: { ..._disabled, _hover: { - background: 'bc-blue', + bg: '#003366 !important', // Use the hex value of bc-blue directly, necessary oddly. + opacity: 0.4, }, }, }, From 952c33d994997293513f85b50ace9d457988403b Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:23:11 -0700 Subject: [PATCH 09/15] match GatewayController activity in OrgController v3 (#1171) * match GatewayController activity in OrgController v3 * openapi update --- src/controllers/v3/OrganizationController.ts | 10 ++++++---- src/controllers/v3/openapi.yaml | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/controllers/v3/OrganizationController.ts b/src/controllers/v3/OrganizationController.ts index 9d717ffef..a4d904cfc 100644 --- a/src/controllers/v3/OrganizationController.ts +++ b/src/controllers/v3/OrganizationController.ts @@ -23,6 +23,7 @@ import { removeKeys, transformAllRefID, syncRecordsThrowErrors, + parseBlobString, } from '../../batch/feed-worker'; import { GroupAccessService, @@ -266,8 +267,8 @@ export class OrganizationController extends Controller { /** * > `Required Scope:` Namespace.Assign * - * @summary Get administration activity for gateways associated with this Organization Unit - * @param orgUnit + * @summary Get administration activity for gateways associated with this Organization + * @param org * @param first * @param skip * @returns Activity[] @@ -299,8 +300,9 @@ export class OrganizationController extends Controller { ); return transformActivity(records) + .map((o) => removeKeys(o, ['id'])) .map((o) => removeEmpty(o)) - .map((o) => transformAllRefID(o, ['blob'])) - .map((o) => parseJsonString(o, ['blob'])); + .map((o) => parseJsonString(o, ['context'])) + .map((o) => parseBlobString(o)); } } diff --git a/src/controllers/v3/openapi.yaml b/src/controllers/v3/openapi.yaml index 2953c2a5f..ec2d79f47 100644 --- a/src/controllers/v3/openapi.yaml +++ b/src/controllers/v3/openapi.yaml @@ -1536,7 +1536,7 @@ paths: $ref: '#/components/schemas/ActivityDetail' type: array description: '> `Required Scope:` Namespace.Assign' - summary: 'Get administration activity for gateways associated with this Organization Unit' + summary: 'Get administration activity for gateways associated with this Organization' tags: - Organizations security: From 0a07cdc1bd5bd75a8232fdc83eb4bc00ddad6e1d Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Fri, 13 Sep 2024 08:03:08 -0700 Subject: [PATCH 10/15] reduce timeout (#1177) --- .../components/no-gateway-redirect/no-gateway-redirect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx index 0407a76c7..c448d5ed1 100644 --- a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx +++ b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx @@ -10,7 +10,7 @@ const NoGatewayRedirect = () => { React.useEffect(() => { const checkNamespaceAndRedirect = async () => { // Wait for a short period to ensure user data is loaded - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise(resolve => setTimeout(resolve, 500)); setIsChecking(false); From fc3499e1e8c3da74abaaf1aa84c82d8adbd25e86 Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Fri, 13 Sep 2024 08:38:52 -0700 Subject: [PATCH 11/15] adjust timeout for redirect --- .../components/no-gateway-redirect/no-gateway-redirect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx index c448d5ed1..303ee16c0 100644 --- a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx +++ b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx @@ -10,7 +10,7 @@ const NoGatewayRedirect = () => { React.useEffect(() => { const checkNamespaceAndRedirect = async () => { // Wait for a short period to ensure user data is loaded - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise(resolve => setTimeout(resolve, 1000)); setIsChecking(false); @@ -42,4 +42,4 @@ const NoGatewayRedirect = () => { return null; }; -export default NoGatewayRedirect; \ No newline at end of file +export default NoGatewayRedirect; From 0122aa17c3fdb9be52c88a14b2e9e5c2f0f94d78 Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Fri, 13 Sep 2024 12:58:23 -0700 Subject: [PATCH 12/15] clickable items in Gateways list --- src/nextapp/pages/manager/gateways/list.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index 6e9389835..d92689d03 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -17,6 +17,7 @@ import Head from 'next/head'; import { gql } from 'graphql-request'; import { FaPlus, FaLaptopCode, FaRocket, FaServer } from 'react-icons/fa'; import { useQueryClient } from 'react-query'; +import { css } from '@emotion/react'; import PageHeader from '@/components/page-header'; import GridLayout from '@/layouts/grid'; @@ -187,6 +188,14 @@ const MyGatewaysPage: React.FC = () => { } }, [data, search, filter]); + const clickableItemStyle = css` + cursor: pointer; + transition: background-color 0.2s; + &:hover { + background-color: #f0f0f0; + } + `; + return ( <> @@ -289,6 +298,8 @@ const MyGatewaysPage: React.FC = () => { py={2} mb={4} data-testid={`ns-list-item-${namespace.name}`} + onClick={handleNamespaceChange(namespace)} + css={clickableItemStyle} > @@ -298,16 +309,15 @@ const MyGatewaysPage: React.FC = () => { mr={4} boxSize={4} /> - {namespace.displayName} - + {namespace.name} From e27499ac89985272e110f2d8511f1d6b522a803a Mon Sep 17 00:00:00 2001 From: Russell Vinegar Date: Fri, 13 Sep 2024 13:05:18 -0700 Subject: [PATCH 13/15] use inline styles --- src/nextapp/pages/manager/gateways/list.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/nextapp/pages/manager/gateways/list.tsx b/src/nextapp/pages/manager/gateways/list.tsx index d92689d03..64b2f14d4 100644 --- a/src/nextapp/pages/manager/gateways/list.tsx +++ b/src/nextapp/pages/manager/gateways/list.tsx @@ -17,7 +17,6 @@ import Head from 'next/head'; import { gql } from 'graphql-request'; import { FaPlus, FaLaptopCode, FaRocket, FaServer } from 'react-icons/fa'; import { useQueryClient } from 'react-query'; -import { css } from '@emotion/react'; import PageHeader from '@/components/page-header'; import GridLayout from '@/layouts/grid'; @@ -188,14 +187,6 @@ const MyGatewaysPage: React.FC = () => { } }, [data, search, filter]); - const clickableItemStyle = css` - cursor: pointer; - transition: background-color 0.2s; - &:hover { - background-color: #f0f0f0; - } - `; - return ( <> @@ -299,7 +290,9 @@ const MyGatewaysPage: React.FC = () => { mb={4} data-testid={`ns-list-item-${namespace.name}`} onClick={handleNamespaceChange(namespace)} - css={clickableItemStyle} + cursor="pointer" + transition="background-color 0.2s" + _hover={{ backgroundColor: "#f0f0f0" }} > From d6e85bb9b104e4d3872db591306bfc515ea5d2ac Mon Sep 17 00:00:00 2001 From: Russell Vinegar <38586679+rustyjux@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:53:10 -0700 Subject: [PATCH 14/15] Feature/gw redirect (#1178) * reduce timeout * use AuthProvider to handle redirecting for gateway pages * use gatewayPages to determine if selector menu is shown * remove your-products from gateways nav item --- src/nextapp/components/nav-bar/nav-bar.tsx | 9 ++-- .../gateway-toast-handler.tsx | 34 -------------- .../components/no-gateway-redirect/index.ts | 1 - .../no-gateway-redirect.tsx | 45 ------------------- src/nextapp/pages/_app.tsx | 18 ++------ src/nextapp/pages/manager/activity/index.tsx | 3 -- .../pages/manager/admin-access/index.tsx | 4 -- .../manager/authorization-profiles/index.tsx | 3 -- src/nextapp/pages/manager/consumers/index.tsx | 3 -- src/nextapp/pages/manager/gateways/detail.tsx | 4 -- src/nextapp/pages/manager/products/index.tsx | 3 -- .../pages/manager/service-accounts/index.tsx | 3 -- src/nextapp/pages/manager/services/index.tsx | 3 -- src/nextapp/shared/data/links.ts | 32 +++++++------ .../shared/services/auth/auth-context.tsx | 19 +++++--- 15 files changed, 41 insertions(+), 143 deletions(-) delete mode 100644 src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx delete mode 100644 src/nextapp/components/no-gateway-redirect/index.ts delete mode 100644 src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx diff --git a/src/nextapp/components/nav-bar/nav-bar.tsx b/src/nextapp/components/nav-bar/nav-bar.tsx index 3858aef4a..1345fa8b0 100644 --- a/src/nextapp/components/nav-bar/nav-bar.tsx +++ b/src/nextapp/components/nav-bar/nav-bar.tsx @@ -4,6 +4,7 @@ import NextLink from 'next/link'; import type { NavLink } from '@/shared/data/links'; import NamespaceMenu from '../namespace-menu'; import { useAuth } from '@/shared/services/auth'; +import { gatewayPages } from '@/shared/data/links'; const linkProps = { px: 4, @@ -48,6 +49,8 @@ const NavBar: React.FC = ({ site, links, pathname }) => { return ''; }, [pathname]); + const requiresNamespace = gatewayPages.includes(pathname); + return ( = ({ site, links, pathname }) => { ))} - {((pathname.startsWith('/manager/') && - pathname !== '/manager/gateways' && - pathname !== '/manager/gateways/get-started' && - pathname !== '/manager/gateways/list') || - pathname === '/devportal/api-directory/your-products') && ( + {requiresNamespace && ( { - const toast = useToast(); - const router = useRouter(); - - React.useEffect(() => { - const handleRouteChange = () => { - const showToast = localStorage.getItem('showNoGatewayToast'); - if (showToast === 'true') { - toast.closeAll(); - toast({ - title: `First select a Gateway to view that page`, - status: 'error', - isClosable: true, - duration: 5000, - }); - localStorage.removeItem('showNoGatewayToast'); - } - }; - - router.events.on('routeChangeComplete', handleRouteChange); - - return () => { - router.events.off('routeChangeComplete', handleRouteChange); - }; - }, [toast, router]); - - return null; -}; - -export default GatewayToastHandler; \ No newline at end of file diff --git a/src/nextapp/components/no-gateway-redirect/index.ts b/src/nextapp/components/no-gateway-redirect/index.ts deleted file mode 100644 index 001eb41b5..000000000 --- a/src/nextapp/components/no-gateway-redirect/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './no-gateway-redirect'; diff --git a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx deleted file mode 100644 index 303ee16c0..000000000 --- a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { useRouter } from 'next/router'; -import { useAuth } from '@/shared/services/auth'; - -const NoGatewayRedirect = () => { - const router = useRouter(); - const { user } = useAuth(); - const [isChecking, setIsChecking] = React.useState(true); - - React.useEffect(() => { - const checkNamespaceAndRedirect = async () => { - // Wait for a short period to ensure user data is loaded - await new Promise(resolve => setTimeout(resolve, 1000)); - - setIsChecking(false); - - if (!user?.namespace) { - try { - // Set localStorage item to show toast - await new Promise((resolve, reject) => { - try { - localStorage.setItem('showNoGatewayToast', 'true'); - resolve(); - } catch (error) { - reject(error); - } - }); - await router.push('/manager/gateways/list'); - } catch (error) { - console.error('Error during redirect process:', error); - } - } - }; - - checkNamespaceAndRedirect(); - }, [user, router]); - - if (isChecking) { - return null; // could return a loading indicator - } - - return null; -}; - -export default NoGatewayRedirect; diff --git a/src/nextapp/pages/_app.tsx b/src/nextapp/pages/_app.tsx index 7cf5709b3..f3bb362ba 100644 --- a/src/nextapp/pages/_app.tsx +++ b/src/nextapp/pages/_app.tsx @@ -18,7 +18,7 @@ import Header from '@/components/header'; import NavBar from '@/components/nav-bar'; import MaintenanceBanner from '@/components/maintenance-banner'; import theme from '@/shared/theme'; -import links from '@/shared/data/links'; +import links, { gatewayPages } from '@/shared/data/links'; import AuthAction from '@/components/auth-action'; import { ReactQueryDevtools } from 'react-query/devtools'; import type { AppProps } from 'next/app'; @@ -31,7 +31,6 @@ import '@/shared/styles/global.css'; import { AppWrapper } from './context'; import '../../mocks'; import CompleteProfile from '@/components/complete-profile'; -import GatewayToastHandler from '@/components/no-gateway-redirect/gateway-toast-handler'; const footerItems = [ { href: 'http://www2.gov.bc.ca/gov/content/home', text: 'Home' }, @@ -57,9 +56,6 @@ const App: React.FC = ({ Component, pageProps }) => { const router = useRouter(); const queryClientRef = React.useRef(); const site: string = React.useMemo(() => { - //if (router?.pathname.startsWith('/manager')) { - // return 'manager'; - //} if (router?.pathname.startsWith('/platform')) { return 'platform'; } @@ -71,12 +67,7 @@ const App: React.FC = ({ Component, pageProps }) => { }, [router]); // Temp solution for handing spacing around new gateways dropdown menu - const gatewaysMenu = - (router?.pathname.startsWith('/manager/') && - router?.pathname !== '/manager/gateways' && - router?.pathname !== '/manager/gateways/get-started' && - router?.pathname !== '/manager/gateways/list') || - router?.pathname === '/devportal/api-directory/your-products'; + const requiresNamespace = gatewayPages.includes(router?.pathname); if (!queryClientRef.current) { queryClientRef.current = new QueryClient({ @@ -118,12 +109,11 @@ const App: React.FC = ({ Component, pageProps }) => { as="main" flex={1} mt={{ - base: gatewaysMenu ? '303px' : '65px', - sm: gatewaysMenu ? '163px' : '115px', + base: requiresNamespace ? '303px' : '65px', + sm: requiresNamespace ? '163px' : '115px', }} > - diff --git a/src/nextapp/pages/manager/activity/index.tsx b/src/nextapp/pages/manager/activity/index.tsx index b747d3eb9..73d19a262 100644 --- a/src/nextapp/pages/manager/activity/index.tsx +++ b/src/nextapp/pages/manager/activity/index.tsx @@ -26,7 +26,6 @@ import ActivityFilters from '@/components/activity-filters'; import { FaTimesCircle } from 'react-icons/fa'; import EmptyPane from '@/components/empty-pane'; import ActivityItem from '@/components/activity-item'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; const timeZone = 'America/Vancouver'; @@ -51,8 +50,6 @@ interface FilterState { } const ActivityPage: React.FC = () => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); const breadcrumbs = useNamespaceBreadcrumbs([ { diff --git a/src/nextapp/pages/manager/admin-access/index.tsx b/src/nextapp/pages/manager/admin-access/index.tsx index 0445d0455..9565dca13 100644 --- a/src/nextapp/pages/manager/admin-access/index.tsx +++ b/src/nextapp/pages/manager/admin-access/index.tsx @@ -21,12 +21,8 @@ import { } from '@/components/namespace-access'; import { useAuth } from '@/shared/services/auth'; import { useNamespaceBreadcrumbs } from '@/shared/hooks'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; const AccessRedirectPage: React.FC = () => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); - const { user } = useAuth(); const breadcrumbs = useNamespaceBreadcrumbs([ { href: '/manager/admin-access', text: 'Administration Access' }, diff --git a/src/nextapp/pages/manager/authorization-profiles/index.tsx b/src/nextapp/pages/manager/authorization-profiles/index.tsx index 2fd8dc866..b2c01cf88 100644 --- a/src/nextapp/pages/manager/authorization-profiles/index.tsx +++ b/src/nextapp/pages/manager/authorization-profiles/index.tsx @@ -41,7 +41,6 @@ import type { Mutation, Query, } from '@/shared/types/query.types'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; export const getServerSideProps: GetServerSideProps = async (context) => { const queryKey = 'authorizationProfiles'; @@ -70,8 +69,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const AuthorizationProfiles: React.FC< InferGetServerSidePropsType > = ({ queryKey }) => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); const breadcrumbs = useNamespaceBreadcrumbs([ { diff --git a/src/nextapp/pages/manager/consumers/index.tsx b/src/nextapp/pages/manager/consumers/index.tsx index 466f4d7ac..2908a6266 100644 --- a/src/nextapp/pages/manager/consumers/index.tsx +++ b/src/nextapp/pages/manager/consumers/index.tsx @@ -36,7 +36,6 @@ import GrantAccessDialog from '@/components/access-request/grant-access-dialog'; import ConsumerFilters from '@/components/consumer-filters'; import AccessRequestsList from '@/components/access-request/access-requests-list'; import { useNamespaceBreadcrumbs } from '@/shared/hooks'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; const sortDate = new Intl.DateTimeFormat('en-ca', { dateStyle: 'short' }); @@ -77,8 +76,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const ConsumersPage: React.FC< InferGetServerSidePropsType > = ({ queryKey }) => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); const toast = useToast(); const breadcrumbs = useNamespaceBreadcrumbs([{ text: 'Consumers' }]); diff --git a/src/nextapp/pages/manager/gateways/detail.tsx b/src/nextapp/pages/manager/gateways/detail.tsx index 4a93ca708..a4d597d9d 100644 --- a/src/nextapp/pages/manager/gateways/detail.tsx +++ b/src/nextapp/pages/manager/gateways/detail.tsx @@ -60,7 +60,6 @@ import useCurrentNamespace from '@/shared/hooks/use-current-namespace'; import { useGlobal } from '@/shared/services/global'; import EditNamespaceDisplayName from '@/components/edit-display-name'; import { useNamespaceBreadcrumbs } from '@/shared/hooks'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; const actions = [ { @@ -120,9 +119,6 @@ const secondaryActions = [ ]; const NamespacesPage: React.FC = () => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); - const { user } = useAuth(); const breadcrumbs = useNamespaceBreadcrumbs(); const hasNamespace = !!user?.namespace; diff --git a/src/nextapp/pages/manager/products/index.tsx b/src/nextapp/pages/manager/products/index.tsx index 6018ab425..26a4eaed2 100644 --- a/src/nextapp/pages/manager/products/index.tsx +++ b/src/nextapp/pages/manager/products/index.tsx @@ -23,11 +23,8 @@ import useCurrentNamespace, { import { useRestMutationApi } from '@/shared/services/api'; import { gql } from 'graphql-request'; import { useAuth } from '@/shared/services/auth'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; const ProductsPage: React.FC = () => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); const { user } = useAuth(); const breadcrumbs = useNamespaceBreadcrumbs([{ text: 'Products' }]); diff --git a/src/nextapp/pages/manager/service-accounts/index.tsx b/src/nextapp/pages/manager/service-accounts/index.tsx index fcd65ed39..751244760 100644 --- a/src/nextapp/pages/manager/service-accounts/index.tsx +++ b/src/nextapp/pages/manager/service-accounts/index.tsx @@ -35,7 +35,6 @@ import { FaCheckCircle } from 'react-icons/fa'; import ServiceAccountCreate from '@/components/service-account-create'; import { useNamespaceBreadcrumbs } from '@/shared/hooks'; import EmptyPane from '@/components/empty-pane'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; export const getServerSideProps: GetServerSideProps = async (context) => { const queryKey = 'getServiceAccounts'; @@ -64,8 +63,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const ServiceAccountsPage: React.FC< InferGetServerSidePropsType > = ({ queryKey }) => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); const breadcrumbs = useNamespaceBreadcrumbs([{ text: 'Service Accounts' }]); const client = useQueryClient(); diff --git a/src/nextapp/pages/manager/services/index.tsx b/src/nextapp/pages/manager/services/index.tsx index bc3199cdd..af9c718db 100644 --- a/src/nextapp/pages/manager/services/index.tsx +++ b/src/nextapp/pages/manager/services/index.tsx @@ -12,7 +12,6 @@ import ServicesFilters from '@/components/services-list/services-filters'; import { useAuth } from '@/shared/services/auth'; import { useNamespaceBreadcrumbs } from '@/shared/hooks'; import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; -import NoGatewayRedirect from '@/components/no-gateway-redirect'; import { FilterState } from '@/components/services-list/types'; @@ -27,8 +26,6 @@ export const getServerSideProps: GetServerSideProps = async () => { const ServicesPage: React.FC< InferGetServerSidePropsType > = ({ metricsUrl }) => { - // Redirect to My Gateways page if no gateway selected - NoGatewayRedirect(); const title = 'Gateway Services'; const breadcrumb = useNamespaceBreadcrumbs([{ text: title }]); diff --git a/src/nextapp/shared/data/links.ts b/src/nextapp/shared/data/links.ts index e3191da12..1c7ef23b7 100644 --- a/src/nextapp/shared/data/links.ts +++ b/src/nextapp/shared/data/links.ts @@ -10,6 +10,24 @@ export interface NavLink { access: string[]; } +export const gatewayPages = [ + '/manager/gateways/detail', + '/manager/services', + '/manager/services/[id]', + '/manager/products', + '/manager/products/[id]', + '/manager/consumers', + '/manager/consumers/[id]', + '/manager/requests/[id]', + '/manager/authorization-profiles', + '/manager/authorization-profiles/new', + '/manager/authorization-profiles/[id]', + '/manager/admin-access', + '/manager/service-accounts', + '/manager/activity', + '/devportal/api-directory/your-products', +]; + const links: NavLink[] = [ // { name: 'Home', url: '/manager', access: [], sites: ['manager'] }, // { name: 'Home', url: '/devportal', access: [], sites: ['devportal'] }, @@ -52,20 +70,8 @@ const links: NavLink[] = [ access: ['idir-user'], altUrls: [ '/manager/gateways/get-started', - '/manager/gateways/detail', '/manager/gateways/list', - '/manager/services', - '/manager/services/[id]', - '/manager/products', - '/manager/products/[id]', - '/manager/consumers', - '/manager/consumers/[id]', - '/manager/requests/[id]', - '/manager/authorization-profiles', - '/manager/authorization-profiles/new', - '/manager/authorization-profiles/[id]', - '/manager/admin-access', - '/manager/service-accounts', + ...gatewayPages.filter(page => !page.startsWith('/devportal')), ], sites: ['devportal'], }, diff --git a/src/nextapp/shared/services/auth/auth-context.tsx b/src/nextapp/shared/services/auth/auth-context.tsx index b2b9a1d3e..d8b6776eb 100644 --- a/src/nextapp/shared/services/auth/auth-context.tsx +++ b/src/nextapp/shared/services/auth/auth-context.tsx @@ -3,6 +3,8 @@ import { Box, Center, Heading, Text } from '@chakra-ui/react'; import Button from '@/components/button'; import links from '@/shared/data/links'; import { useRouter } from 'next/router'; +import { gatewayPages } from '@/shared/data/links'; +import { useToast } from '@chakra-ui/react'; import { useSession, UserSessionResult } from './use-session'; @@ -19,6 +21,7 @@ interface AuthProviderProps { export const AuthProvider: React.FC = ({ children }) => { const session = useSession(); + const toast = useToast(); const router = useRouter(); const route = links.find( (d) => d.url === router?.pathname || d.altUrls?.includes(router?.pathname) @@ -38,16 +41,22 @@ export const AuthProvider: React.FC = ({ children }) => { // A logged in user trying to access a Namespace'd page (page that is not protected with "portal-user" role) // and no namespace set, then redirect to Gateways page + const requiresNamespace = gatewayPages.includes(router?.pathname); + const noNamespace = session.user && - route?.access && - route?.access.length > 0 && - route?.access.indexOf('portal-user') == -1 && - route?.access.indexOf('idir-user') == -1 && + requiresNamespace && !session.user.namespace; if (noNamespace) { - router?.push('/manager/gateways'); + router?.push('/manager/gateways/list').then(() => { + toast({ + title: `First select a Gateway to view that page`, + status: 'error', + isClosable: true, + duration: 5000, + }); + }); return <>; } From 73f309dce2c2e04f346bb18dc08bed767670a3b4 Mon Sep 17 00:00:00 2001 From: James Elson Date: Thu, 19 Sep 2024 11:35:05 -0700 Subject: [PATCH 15/15] Resolve conflicts pt.2 --- .../gateway-toast-handler.tsx | 34 ------------------- .../no-gateway-redirect.tsx | 0 src/nextapp/pages/_app.tsx | 2 -- 3 files changed, 36 deletions(-) delete mode 100644 src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx delete mode 100644 src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx diff --git a/src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx b/src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx deleted file mode 100644 index b67ca18e5..000000000 --- a/src/nextapp/components/no-gateway-redirect/gateway-toast-handler.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { useToast } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; - -const GatewayToastHandler = () => { - const toast = useToast(); - const router = useRouter(); - - React.useEffect(() => { - const handleRouteChange = () => { - const showToast = localStorage.getItem('showNoGatewayToast'); - if (showToast === 'true') { - toast.closeAll(); - toast({ - title: `First select a Gateway to view that page`, - status: 'error', - isClosable: true, - duration: 5000, - }); - localStorage.removeItem('showNoGatewayToast'); - } - }; - - router.events.on('routeChangeComplete', handleRouteChange); - - return () => { - router.events.off('routeChangeComplete', handleRouteChange); - }; - }, [toast, router]); - - return null; -}; - -export default GatewayToastHandler; \ No newline at end of file diff --git a/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx b/src/nextapp/components/no-gateway-redirect/no-gateway-redirect.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/nextapp/pages/_app.tsx b/src/nextapp/pages/_app.tsx index 2aeeeca2b..f3bb362ba 100644 --- a/src/nextapp/pages/_app.tsx +++ b/src/nextapp/pages/_app.tsx @@ -31,7 +31,6 @@ import '@/shared/styles/global.css'; import { AppWrapper } from './context'; import '../../mocks'; import CompleteProfile from '@/components/complete-profile'; -import GatewayToastHandler from '@/components/no-gateway-redirect/gateway-toast-handler'; const footerItems = [ { href: 'http://www2.gov.bc.ca/gov/content/home', text: 'Home' }, @@ -115,7 +114,6 @@ const App: React.FC = ({ Component, pageProps }) => { }} > -