From f8672f8a90c9701dc312f92e79efad0bc16205e3 Mon Sep 17 00:00:00 2001 From: David Ma <40131297+davidma415@users.noreply.github.com> Date: Thu, 1 Feb 2024 14:06:51 -0700 Subject: [PATCH] feat: disable new apps if no auth strategies and display banner (#383) --- .../specs/application_registration.spec.ts | 59 +++++++++++++++++++ cypress/e2e/support/index.ts | 3 +- cypress/e2e/support/mock-commands.ts | 19 ++++++ package.json | 2 +- src/constants/feature-flags.ts | 3 +- src/locales/ca_ES.ts | 2 + src/locales/de.ts | 2 + src/locales/en.ts | 2 + src/locales/es_ES.ts | 2 + src/locales/fr.ts | 2 + src/locales/i18n-type.d.ts | 2 + src/views/Applications/ApplicationForm.vue | 34 ++++++++++- src/views/MyApps.vue | 36 ++++++++++- yarn.lock | 8 +-- 14 files changed, 166 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/specs/application_registration.spec.ts b/cypress/e2e/specs/application_registration.spec.ts index 09008a4e..cf51bef0 100644 --- a/cypress/e2e/specs/application_registration.spec.ts +++ b/cypress/e2e/specs/application_registration.spec.ts @@ -215,6 +215,65 @@ describe('Application Registration', () => { cy.get('[data-testid="reference-id-input"]').should('not.have.value', '') }) + + it('appregv2 - create application form shows banner if no auth strategies and flag enabled', () => { + cy.mockLaunchDarklyFlags([ + { + name: 'tdx-3531-app-reg-v2', + value: true + } + ]) + cy.mockApplicationAuthStrategies([], 0) + cy.visit('/application/create') + + cy.get('[data-testid="application-name-input"]').type(apps[0].name, { delay: 0 }) + cy.get('#description').type(apps[0].description, { delay: 0 }) + cy.get('[data-testid="reference-id-input"]').type(apps[0].reference_id, { delay: 0 }) + cy.get(submitButton).should('be.disabled') + cy.get('[data-testid="no-auth-strategies-warning"]').should('be.visible') + }) + + it('appregv2 - create application form does not show banner if flag disabled', () => { + cy.mockLaunchDarklyFlags([ + { + name: 'tdx-3531-app-reg-v2', + value: false + } + ]) + cy.mockApplicationAuthStrategies([], 0) + cy.visit('/application/create') + + cy.get('[data-testid="no-auth-strategies-warning"]').should('not.exist') + }) + it('appregv2 - does not show warning banner if flag is not on', () => { + cy.mockLaunchDarklyFlags([ + { + name: 'tdx-3531-app-reg-v2', + value: false + } + ]) + cy.mockApplications([], 0) + cy.mockApplicationAuthStrategies([], 0) + cy.visit('/my-apps') + + cy.get('[data-testid="create-application-button"]').should('not.be.disabled') + cy.get('[data-testid="no-auth-strategies-warning"]').should('not.exist') + }) + + it('appregv2 - shows warning banner if no available auth strategies', () => { + cy.mockLaunchDarklyFlags([ + { + name: 'tdx-3531-app-reg-v2', + value: true + } + ]) + cy.mockApplications([], 0) + cy.mockApplicationAuthStrategies([], 0) + cy.visit('/my-apps') + + cy.get('[data-testid="create-application-button"]').should('have.attr', 'disabled', 'disabled') + cy.get('[data-testid="no-auth-strategies-warning"]').should('be.visible') + }) }) it('can return to My Apps from application details via breadcrumb', () => { diff --git a/cypress/e2e/support/index.ts b/cypress/e2e/support/index.ts index 1f78853b..9b5d8078 100644 --- a/cypress/e2e/support/index.ts +++ b/cypress/e2e/support/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ // Import commands.js using ES2015 syntax: -import { GetApplicationResponse, GetRegistrationResponse, ListCredentialsResponseDataInner, PortalAppearance, PortalContext, Product, ProductCatalogIndexSource, ProductVersion, ProductVersionSpecOperationsOperationsInner } from '@kong/sdk-portal-js' +import { GetApplicationResponse, GetRegistrationResponse, ListAuthStrategiesItem, ListCredentialsResponseDataInner, PortalAppearance, PortalContext, Product, ProductCatalogIndexSource, ProductVersion, ProductVersionSpecOperationsOperationsInner } from '@kong/sdk-portal-js' import './mock-commands' import { SinonStub } from 'cypress/types/sinon' @@ -26,6 +26,7 @@ declare global { mockProduct(productId?: string, mockProduct?: Product, mockVersions?: ProductVersion[]): Chainable> mockProductVersion(productId?: string, versionId?: string, mockVersion?: ProductVersion): Chainable> mockApplications(searchResults?: Array, totalCount?: number, pageSize?: number, pageNumber?: number): Chainable> + mockApplicationAuthStrategies(authStrategyItems?: Array, totalCount?: number, pageSize?: number, pageNumber?: number): Chainable> mockApplicationWithCredAndReg(data: GetApplicationResponse, credentials?: ListCredentialsResponseDataInner[], registrations?: Array): Chainable>, mockContextualAnalytics(): Chainable> mockRegistrations(applicationId?: string, registrations?: Array, totalCount?: number): Chainable> diff --git a/cypress/e2e/support/mock-commands.ts b/cypress/e2e/support/mock-commands.ts index b87e46d3..e640467d 100644 --- a/cypress/e2e/support/mock-commands.ts +++ b/cypress/e2e/support/mock-commands.ts @@ -10,6 +10,7 @@ import petstoreOperationsV2 from '../fixtures/v2/petstoreOperations.json' import { GetApplicationResponse, ListApplicationsResponse, + ListAuthStrategiesResponse, ListCredentialsResponse, ListDocumentsTree, ListRegistrationsResponse, @@ -333,6 +334,24 @@ Cypress.Commands.add('mockApplications', (applications, totalCount, pageSize = 1 }).as('getApplications') }) +Cypress.Commands.add('mockApplicationAuthStrategies', (applicationAuthStrategies, totalCount, pageSize = 1, pageNumber = 10) => { + const responseBody: ListAuthStrategiesResponse = { + data: applicationAuthStrategies, + meta: { + page: { + total: totalCount, + number: pageNumber, + size: pageSize + + } + } + } + + return cy.intercept('GET', '**/api/v2/applications/auth-strategies*', { + body: responseBody + }).as('getApplicationAuthStrategies') +}) + Cypress.Commands.add('mockRegistrations', (applicationId = '*', registrations = [], pageNumber = 1, pageSize = 10, totalCount = 0) => { return cy.intercept('GET', `**/api/v2/applications/${applicationId}/registrations*`, { body: { diff --git a/package.json b/package.json index bc240a84..2683d5df 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@kong-ui-public/spec-renderer": "0.13.5", "@kong/kong-auth-elements": "2.11.1", "@kong/kongponents": "8.127.0", - "@kong/sdk-portal-js": "2.5.0", + "@kong/sdk-portal-js": "2.6.1", "@xstate/vue": "2.0.0", "axios": "1.6.0", "date-fns": "3.3.0", diff --git a/src/constants/feature-flags.ts b/src/constants/feature-flags.ts index 3e326b0c..a14d2716 100644 --- a/src/constants/feature-flags.ts +++ b/src/constants/feature-flags.ts @@ -1,3 +1,4 @@ export enum FeatureFlags { - DeveloperManagedScopes = 'tdx-3460-developer-managed-scopes' + DeveloperManagedScopes = 'tdx-3460-developer-managed-scopes', + AppRegV2 = 'tdx-3531-app-reg-v2' } diff --git a/src/locales/ca_ES.ts b/src/locales/ca_ES.ts index 44772230..2b398fa4 100644 --- a/src/locales/ca_ES.ts +++ b/src/locales/ca_ES.ts @@ -103,6 +103,7 @@ export const ca_ES: I18nType = { delete: 'Eliminar', proceed: 'Continuar', applicationName: "Nom de l'aplicació ", + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), clientID: 'ID de client: ', clientSecret: 'Clau secreta de client: ', reqField: ' indica un camp obligatori', @@ -285,6 +286,7 @@ export const ca_ES: I18nType = { logoAlt: 'logotip' }, myApp: { + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), newApp: 'Nova aplicació', plus: 'Més', myApps: 'Les meves aplicacions', diff --git a/src/locales/de.ts b/src/locales/de.ts index 40a1efed..5a9ac041 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -103,6 +103,7 @@ export const de: I18nType = { delete: 'Löschen', proceed: 'Weiter', applicationName: 'Name der Applikation', + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), clientID: translationNeeded(en.application.clientID), clientSecret: translationNeeded(en.application.clientSecret), reqField: ' Pflichtfeld', @@ -285,6 +286,7 @@ export const de: I18nType = { logoAlt: 'Logo' }, myApp: { + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), newApp: 'Neue Applikation', plus: 'Plus', myApps: 'Meine Applikationen', diff --git a/src/locales/en.ts b/src/locales/en.ts index bc11970f..fa20938f 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -99,6 +99,7 @@ export const en = { delete: 'Delete', proceed: 'Proceed', applicationName: 'Application Name ', + authStrategyWarning: 'You cannot create an application as this developer portal has no available application auth strategies. Please contact a developer portal admin.', clientID: 'Client ID: ', clientSecret: 'Client Secret: ', reqField: ' indicates required field', @@ -281,6 +282,7 @@ export const en = { logoAlt: 'logo' }, myApp: { + authStrategyWarning: 'You cannot create an application as this developer portal has no available application auth strategies. Please contact a developer portal admin.', newApp: 'New App', plus: 'Plus', myApps: 'My Apps', diff --git a/src/locales/es_ES.ts b/src/locales/es_ES.ts index bc39eb3c..33a9ac8c 100644 --- a/src/locales/es_ES.ts +++ b/src/locales/es_ES.ts @@ -103,6 +103,7 @@ export const es_ES: I18nType = { delete: 'Eliminar', proceed: 'Continuar', applicationName: 'Nombre de la aplicación ', + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), clientID: 'ID de cliente: ', clientSecret: 'Clave secreta de cliente: ', reqField: ' indica campo obligatorio', @@ -285,6 +286,7 @@ export const es_ES: I18nType = { logoAlt: 'logo' }, myApp: { + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), newApp: 'Nueva aplicación', plus: 'Plus', myApps: 'Mis aplicaciones', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 21532b05..9dddd598 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -103,6 +103,7 @@ export const fr: I18nType = { delete: 'Supprimer', proceed: 'Continuer', applicationName: 'Nom de l\'application ', + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), clientID: 'Client ID : ', clientSecret: 'Client Secret : ', reqField: ' indique un champ obligatoire', @@ -285,6 +286,7 @@ export const fr: I18nType = { logoAlt: 'logo' }, myApp: { + authStrategyWarning: translationNeeded(en.application.authStrategyWarning), newApp: 'Nouvelle application', plus: 'Plus', myApps: 'Mes applications', diff --git a/src/locales/i18n-type.d.ts b/src/locales/i18n-type.d.ts index f4810f5e..317ed3ae 100644 --- a/src/locales/i18n-type.d.ts +++ b/src/locales/i18n-type.d.ts @@ -99,6 +99,7 @@ export interface I18nType { delete: string; proceed: string; applicationName: string; + authStrategyWarning: string; clientID: string; clientSecret: string; reqField: string; @@ -281,6 +282,7 @@ export interface I18nType { logoAlt: string; }; myApp: { + authStrategyWarning: string; newApp: string; plus: string; myApps: string; diff --git a/src/views/Applications/ApplicationForm.vue b/src/views/Applications/ApplicationForm.vue index 203efc36..2def4d7b 100644 --- a/src/views/Applications/ApplicationForm.vue +++ b/src/views/Applications/ApplicationForm.vue @@ -15,6 +15,13 @@

* {{ helpText.application.reqField }}

+
@@ -207,6 +214,8 @@ import cleanupEmptyFields from '@/helpers/cleanupEmptyFields' import useToaster from '@/composables/useToaster' import { useI18nStore, useAppStore } from '@/stores' import { CreateApplicationPayload, UpdateApplicationPayload } from '@kong/sdk-portal-js' +import { FeatureFlags } from '@/constants/feature-flags' +import useLDFeatureFlag from '@/hooks/useLDFeatureFlag' export default defineComponent({ name: 'ApplicationForm', @@ -238,6 +247,8 @@ export default defineComponent({ const applicationId = ref('') const applicationName = ref('') const secretModalIsVisible = ref(false) + const appRegV2Enabled = useLDFeatureFlag(FeatureFlags.AppRegV2, false) + const hasAppAuthStrategies = ref(false) const defaultFormData: UpdateApplicationPayload = makeDefaultFormData(isDcr.value) const formData = ref(defaultFormData) @@ -274,6 +285,7 @@ export default defineComponent({ () => !currentState.value.matches('pending') && formData.value.name.length && + (appRegV2Enabled && formMode.value !== 'edit' ? hasAppAuthStrategies.value : true) && (isDcr.value ? true : formData.value.reference_id?.length) ) const modalTitle = computed(() => `Delete ${formData.value?.name}`) @@ -291,10 +303,24 @@ export default defineComponent({ const { portalApiV2 } = usePortalApi() - onMounted(() => { + onMounted(async () => { if (id.value) { fetchApplication() } + + if (appRegV2Enabled) { + try { + const appAuthStrategies = await portalApiV2.value.service.applicationsApi.listApplicationAuthStrategies() + if (appAuthStrategies.data?.data?.length) { + hasAppAuthStrategies.value = true + } + } catch (err) { + notify({ + appearance: 'danger', + message: `Error fetching application auth strategies: ${err}` + }) + } + } }) const copyTokenToClipboard = (executeCopy, copyItem) => { @@ -459,6 +485,8 @@ export default defineComponent({ copyTokenToClipboard, secretModalIsVisible, handleAcknowledgeSecret, + hasAppAuthStrategies, + appRegV2Enabled, send, buttonText, formMode, @@ -490,4 +518,8 @@ export default defineComponent({ top: 4px; margin-left: 16px; } + +.no-auth-strategies-warning { + margin-bottom: 8px; +} diff --git a/src/views/MyApps.vue b/src/views/MyApps.vue index bb74e97b..22d408ce 100644 --- a/src/views/MyApps.vue +++ b/src/views/MyApps.vue @@ -8,6 +8,7 @@ @@ -31,6 +32,13 @@ +
@@ -205,6 +213,8 @@ import '@kong-ui-public/analytics-metric-provider/dist/style.css' import { EXPLORE_V2_DIMENSIONS, EXPLORE_V2_FILTER_TYPES, MetricsConsumer } from '@kong-ui-public/analytics-metric-provider' import { storeToRefs } from 'pinia' +import { FeatureFlags } from '@/constants/feature-flags' +import useLDFeatureFlag from '@/hooks/useLDFeatureFlag' export default defineComponent({ name: 'MyApps', @@ -221,6 +231,8 @@ export default defineComponent({ const showSecretModal = ref(false) const token = ref(null) const { portalApiV2 } = usePortalApi() + const appRegV2Enabled = useLDFeatureFlag(FeatureFlags.AppRegV2, false) + const hasAppAuthStrategies = ref(false) const appStore = useAppStore() const { isDcr } = storeToRefs(appStore) @@ -352,8 +364,22 @@ export default defineComponent({ ] })) - onMounted(() => { + onMounted(async () => { vitalsLoading.value = false + + if (appRegV2Enabled) { + try { + const appAuthStrategies = await portalApiV2.value.service.applicationsApi.listApplicationAuthStrategies() + if (appAuthStrategies.data?.data?.length) { + hasAppAuthStrategies.value = true + } + } catch (err) { + notify({ + appearance: 'danger', + message: `Error fetching application auth strategies: ${err}` + }) + } + } }) return { @@ -366,6 +392,8 @@ export default defineComponent({ isDcr, deleteItem, showSecretModal, + appRegV2Enabled, + hasAppAuthStrategies, token, onModalClose, handleRefreshSecret, @@ -384,9 +412,13 @@ export default defineComponent({ }) - diff --git a/yarn.lock b/yarn.lock index 3f8c86e5..be2d9875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1121,10 +1121,10 @@ v-calendar "3.0.0-alpha.8" vue-draggable-next "^2.2.1" -"@kong/sdk-portal-js@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@kong/sdk-portal-js/-/sdk-portal-js-2.5.0.tgz#bfa53c3fcb0fbd63cb54eea2e73443a34fab3fa6" - integrity sha512-nt74ub5WpLF2Vh/dVeHIycccaNr9wVOfcvDaGnoccfH41Pi5jtBa/5lJSEG2hMSRloRVAX8J6TRWT5vHNYCiKg== +"@kong/sdk-portal-js@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@kong/sdk-portal-js/-/sdk-portal-js-2.6.1.tgz#b855a505c0166825fcd4494f385a61bd7e5039e6" + integrity sha512-Q5Vcw57j48F+ujhGgtbTWyvshZBJgX/VKoZgEeRiJABAdfN7Td9CB+8UCAvK2uhk4kB0lzh/HCVqv9kCD1IhAQ== dependencies: axios "1.6.0"