From 8cc7256d464b76426e5e7e429b952cee72c64235 Mon Sep 17 00:00:00 2001 From: Ross Kukulinski Date: Sun, 20 Aug 2023 23:22:45 -0700 Subject: [PATCH] Prod rebase (#16) * build(deps): bump @kong-ui-public/copy-uuid from 0.3.15 to 0.6.0 (#170) Bumps [@kong-ui-public/copy-uuid](https://github.com/Kong/public-ui-components/tree/HEAD/packages/core/copy-uuid) from 0.3.15 to 0.6.0. - [Release notes](https://github.com/Kong/public-ui-components/releases) - [Changelog](https://github.com/Kong/public-ui-components/blob/main/packages/core/copy-uuid/CHANGELOG.md) - [Commits](https://github.com/Kong/public-ui-components/commits/@kong-ui-public/copy-uuid@0.6.0/packages/core/copy-uuid) --- updated-dependencies: - dependency-name: "@kong-ui-public/copy-uuid" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @kong/kongponents from 8.98.0 to 8.99.0 (#174) Bumps [@kong/kongponents](https://github.com/Kong/kongponents) from 8.98.0 to 8.99.0. - [Release notes](https://github.com/Kong/kongponents/releases) - [Changelog](https://github.com/Kong/kongponents/blob/main/CHANGELOG.md) - [Commits](https://github.com/Kong/kongponents/compare/v8.98.0...v8.99.0) --- updated-dependencies: - dependency-name: "@kong/kongponents" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(ci): add lint and type check as required for tests (#176) * ci: use semantic-release to generate releases and changelog (#175) * ci: check PR title instead of commits (#178) * style(login): update styling of sso button on login view (#179) * build(deps): bump @kong/kong-auth-elements from 2.1.0 to 2.7.1 (#183) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @kong-ui-public/copy-uuid from 0.6.0 to 0.7.5 (#188) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @kong-ui-public/document-viewer from 0.9.0 to 0.10.5 (#189) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @kong-ui-public/spec-renderer from 0.10.0 to 0.11.6 (#190) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @kong/kongponents from 8.99.0 to 8.116.2 (#187) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(ci): update sync-changes.yml to use proper bot token (#191) * fix(auth): update package to fix register request payload (#192) * build(deps): bump eslint-plugin-vue from 9.15.0 to 9.16.1 (#186) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * style: fix css variables for document viewer (#193) * build(deps): bump eslint from 8.45.0 to 8.46.0 (#180) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: improve empty states for spec / doc rendering (#194) * chore: update readme for domain clarification (#195) * feat: add check for meta.public for public routes (#196) * chore(deps): bump portal sdk to 2.1 (#201) * fix: update variable names for document viewer (#203) * feat(app-analytics): expose SDK for application analytics (#204) * build(deps): bump @kong/kong-auth-elements from 2.7.2 to 2.8.0 (#199) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs: add information to readme regarding public directory (#205) * build(deps): bump @kong-ui-public/copy-uuid from 0.7.5 to 1.1.5 (#206) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @typescript-eslint/parser from 5.61.0 to 5.62.0 (#156) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump eslint-plugin-import from 2.27.5 to 2.28.0 (#185) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * refactor: dont query catalog if switching views (#208) * refactor: redirect to 404 when spec is not found (#215) * feat(vitals): migrate contextual analytics from monolith [MA-1788] (#133) * fix(analytics): port over the contextual analytics spec (#216) * Add netlify.toml --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Devon Langendoerfer Co-authored-by: Andrew Wylde Co-authored-by: David Ma <40131297+davidma415@users.noreply.github.com> Co-authored-by: Mike Swierenga Co-authored-by: Mihai <103061463+mihai-peteu@users.noreply.github.com> --- .github/workflows/check-pr-title.yml | 19 + .github/workflows/pr.yml | 2 +- .github/workflows/release.yml | 22 + .github/workflows/sync-changes.yml | 23 + .releaserc.yaml | 45 + README.md | 93 +- cypress/e2e/fixtures/consts.ts | 6 +- cypress/e2e/specs/api_documentation.spec.ts | 13 +- .../specs/application_registration.spec.ts | 5 +- .../e2e/specs/contextual-analytics.spec.ts | 114 + cypress/e2e/support/index.ts | 4 +- cypress/e2e/support/mock-commands.ts | 74 +- lefthook.yaml | 9 - package.json | 32 +- src/assets/mixins.scss | 8 + src/assets/variables.scss | 6 + src/components/Catalog.vue | 1 + src/components/CatalogTableList.vue | 5 + src/components/product/Sidebar.vue | 6 + src/components/registerComponents.ts | 2 + src/components/vitals/AnalyticsEmptyState.vue | 57 + .../vitals/AnalyticsMetricsCard.vue | 68 + src/components/vitals/ChartPanel.vue | 279 ++ src/components/vitals/MetricsProvider.vue | 82 + src/composables/useAllowedTimeframes.ts | 60 + src/composables/useChartQueryBuilder.ts | 26 + src/composables/useChartRequest.ts | 52 + src/constants/chartQueries.ts | 73 + src/constants/feature-flags.ts | 1 + src/helpers/snakeToCamelCase.ts | 17 + src/locales/ca_ES.ts | 34 +- src/locales/de.ts | 34 +- src/locales/en.ts | 34 +- src/locales/es_ES.ts | 34 +- src/locales/fr.ts | 34 +- src/locales/i18n-type.d.ts | 32 + src/main.ts | 5 +- src/router/index.ts | 10 + src/services/PortalV2ApiService.ts | 9 +- src/stores/app.ts | 8 + src/types/productVersion.ts | 9 + src/types/vitals.ts | 19 + src/views/ApiDocumentationPage.vue | 34 +- .../Applications/ApplicationDashboard.vue | 366 ++ src/views/Applications/ApplicationDetail.vue | 64 +- src/views/Login.vue | 13 + src/views/MyApps.vue | 82 +- src/views/ProductCatalogWrapper.vue | 11 +- src/views/ProductShell.vue | 15 +- src/views/Spec.vue | 13 +- yarn.lock | 3526 +++++++++++++++-- 51 files changed, 5141 insertions(+), 449 deletions(-) create mode 100644 .github/workflows/check-pr-title.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/sync-changes.yml create mode 100644 .releaserc.yaml create mode 100644 cypress/e2e/specs/contextual-analytics.spec.ts create mode 100644 src/assets/mixins.scss create mode 100644 src/assets/variables.scss create mode 100644 src/components/vitals/AnalyticsEmptyState.vue create mode 100644 src/components/vitals/AnalyticsMetricsCard.vue create mode 100644 src/components/vitals/ChartPanel.vue create mode 100644 src/components/vitals/MetricsProvider.vue create mode 100644 src/composables/useAllowedTimeframes.ts create mode 100644 src/composables/useChartQueryBuilder.ts create mode 100644 src/composables/useChartRequest.ts create mode 100644 src/constants/chartQueries.ts create mode 100644 src/helpers/snakeToCamelCase.ts create mode 100644 src/types/productVersion.ts create mode 100644 src/types/vitals.ts create mode 100644 src/views/Applications/ApplicationDashboard.vue diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml new file mode 100644 index 00000000..bedaa8e4 --- /dev/null +++ b/.github/workflows/check-pr-title.yml @@ -0,0 +1,19 @@ +name: Check PR title + +on: + pull_request_target: + types: + - opened + - reopened + - edited + - synchronize + +jobs: + lint: + runs-on: ubuntu-latest + permissions: + statuses: write + steps: + - uses: aslafy-z/conventional-pr-title-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a1ac0808..d5c9d405 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -11,7 +11,6 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: wagoid/commitlint-github-action@v5 - name: Check unpinned versions run: ./.github/scripts/pin-version - name: node modules cache @@ -32,6 +31,7 @@ jobs: tests: name: Tests runs-on: ubuntu-latest + needs: code-quality strategy: fail-fast: false matrix: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..6dfe9177 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Semantic Release +on: + workflow_dispatch: +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Semantic Release + env: + GITHUB_TOKEN: ${{ secrets.GHA_TEAM_DEVX_KONG_BOT_PAT }} + run: npx semantic-release diff --git a/.github/workflows/sync-changes.yml b/.github/workflows/sync-changes.yml new file mode 100644 index 00000000..b1477be8 --- /dev/null +++ b/.github/workflows/sync-changes.yml @@ -0,0 +1,23 @@ +name: Sync Changes From Release Tag +on: + release: + types: published +jobs: + merge: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: release + fetch-depth: 0 + token: ${{ secrets.GHA_TEAM_DEVX_KONG_BOT_PAT }} + - name: Override release branch with changes from release tag + shell: bash + env: + GH_TOKEN: ${{ secrets.GHA_TEAM_DEVX_KONG_BOT_PAT }} + run: | + git config --global user.email "team-devx+github-bot@konghq.com" + git config --global user.name "team-devx" + git reset --hard ${{ github.event.release.tag_name }} + git push --force-with-lease origin release diff --git a/.releaserc.yaml b/.releaserc.yaml new file mode 100644 index 00000000..2fccea5d --- /dev/null +++ b/.releaserc.yaml @@ -0,0 +1,45 @@ +{ + "branches": ["main"], + "tagFormat": "${version}", + "plugins": [ + ["@semantic-release/commit-analyzer", { + "preset": "conventionalcommits", + "releaseRules": [ + { "breaking": true, "release": "major" }, + { "revert": true, "release": "patch" }, + { "type": "build", "release": "patch" }, + { "type": "chore", "release": "patch" }, + { "type": "docs", "release": "patch" }, + { "type": "feat", "release": "minor" }, + { "type": "fix", "release": "patch" }, + { "type": "perf", "release": "patch" }, + { "type": "refactor", "release": "patch" } + ] + }], + ["@semantic-release/release-notes-generator", { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { "type": "build", "section": "Build", "hidden": false }, + { "type": "chore", "section": "Chores", "hidden": false }, + { "type": "ci", "section": "CI/CD", "hidden": false }, + { "type": "docs", "section": "Docs", "hidden": false }, + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "perf", "section": "Performance", "hidden": false }, + { "type": "refactor", "section": "Refactor", "hidden": false }, + { "type": "style", "section": "Code Style", "hidden": false }, + { "type": "test", "section": "Tests", "hidden": false } + ] + } + }], + "@semantic-release/github", + ["@semantic-release/git", { + "assets": ["CHANGELOG.md", "package.json", "yarn.lock"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + }], + "@semantic-release/changelog" + ], + "successComment": false +} + diff --git a/README.md b/README.md index d143461d..9993a25d 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ ![License](https://img.shields.io/badge/License-Apache%202.0-blue?style=flat-square) ![Twitter Follow](https://img.shields.io/twitter/follow/thekonginc?style=social) + # Konnect Dev Portal Client This repo is an [open source][oss-url] reference implementation of a Konnect Developer Portal Client leveraging the [Konnect Developer Portal Client API][portal-api-url] and [JavaScript SDK][javscript-sdk-url]. -The [Konnect Dev Portal][konnect-docs-url] is a web application for developers to locate, access, and consume API services. The Dev Portal enables developers to browse and search API documentation, test API endpoints, and manage their own credentials. +The [Konnect Dev Portal][konnect-docs-url] is a web application for developers to locate, access, and consume API services. The Dev Portal enables developers to browse and search API documentation, test API endpoints, and manage their own credentials. In [Kong Konnect][kong-konnect-register-url], you have two hosting options for the Dev Portal web user interface: a cloud hosted Dev Portal with Konnect or a self-hosted, open source Dev Portal powered by Konnect APIs. @@ -29,17 +30,50 @@ With those benefits in mind, there _is_ the hosting cost to deploy this single p ## Getting started -### Prerequisites: +### Using the Project: Best Practices + +### Branches + +1. **Main Branch (`main`)**: The `main` branch serves as the default branch, and all commits and pull requests should be directed here. It represents the latest version of the project. + +2. **Release Branch (`release`)**: The `release` branch includes all the changes from the `main` branch, but its latest commit will always correspond to the latest release tag. + +### Choosing the Right Branch + +When contributing or using the project, it is essential to understand which branch best suits your needs: + +1. **For Contributors**: + +* If you want to contribute any new features or bug fixes, please create a new branch based on the `main` branch. Name your branch descriptively ([see the branch naming conventions](#branch-naming-conventions) - Open a pull request to merge your changes into the `main` branch. This allows the maintainers to review your code before merging it into the default branch. + +2. **For Users Who Want Frequent Updates**: + +* If you prefer to use the latest development version of the project, you should use the `main` branch directly, either in your fork or as a submodule of your project. + +3. **For Users Who Want Stable Releases**: + +* Use the `release` branch if you prefer less-frequent updates. - The `release` branch provides a production-ready version of the project at each tagged release. - It is recommended to keep your fork of the repository updated with the latest changes from the `release` branch. + +### Staying Updated + +Whether you are contributing or using the project, staying updated is crucial: + +* Regularly fetch and pull changes from the `main` branch to your local repository. This ensures that your work is based on the most recent codebase. +* If you are using the `release` branch, merge the latest changes from `main` into your fork periodically to keep it up-to-date with the latest releases. + +By following these guidelines, both contributors and users can efficiently collaborate on and use the project, ensuring a smooth and productive development experience for everyone involved. + +### Prerequisites * Kong Konnect account - * You can Start a Free trial at: [konghq.com][kong-konnect-register-url] - * Documentation for Kong Konnect is available at: [docs.konghq.com][konnect-docs-url] + * You can Start a Free trial at: [konghq.com][kong-konnect-register-url] + * Documentation for Kong Konnect is available at: [docs.konghq.com][konnect-docs-url] * Yarn [^1.22.x][yarn-install-url] Install dependencies ```sh -yarn +yarn install --frozen-lockfile ``` Create local .env file @@ -48,7 +82,7 @@ Create local .env file cp .env.example .env ``` -Set `VITE_PORTAL_API_URL` value in your current environment i.e .env file or local environment, this should match either the Kong supplied portal URL ending in `portal.konghq.com` or the [custom Portal URL set in Konnect][custom-dev-portal-url]. Be sure to set the Custom Client domain to match the domain you will be serving the portal out of to avoid CORS issues. +Set `VITE_PORTAL_API_URL` value in your current environment i.e .env file or local environment, this should match either the Kong supplied portal URL ending in `portal.konghq.com` (for local development) or the [custom hosted domain URL set in Konnect][custom-dev-portal-url] (for your deployed environment). Be sure to set the custom self-hosted UI domain to match the domain you will be serving the portal out of to avoid CORS issues. For Development you can provide any portal API URL, it is proxied by Vite, so you do not need to set the custom client domain. @@ -59,10 +93,15 @@ yarn dev #optional --verbose ``` Run tests with + ```sh yarn test:e2e ``` +### Public Directory + +If you need to store assets (e.g. fonts, images, or icons), you can create a `public` directory at the root level of the repository and Vite will utilize it by default. For more information on when or how to use the public folder, visit [here](https://vitejs.dev/guide/assets.html#the-public-directory). + ## Building for production release Build production bundle '_(dist/)_' for deployment with @@ -78,36 +117,46 @@ First and foremost please and comply with the standards outlined in the [CODE_OF ### Committing Changes - Please follow the following branch naming scheme when creating your branch: This repo uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). [Commitizen](https://github.com/commitizen/cz-cli) can be used to help build commit messages. + Please follow the following branch naming scheme when creating your branch: This repo uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). [Commitizen](https://github.com/commitizen/cz-cli) can be used to help build commit messages. Or you can utilize the installed version with any of the following commands: + + ```sh + yarn commit + ``` #### _Note:_ - _To disable commit formatting on your fork you can either comment out the contents of the lefthook.yaml file or remove it, as well as uninstall lefthook from the package.json file._ + _To disable linting during the `pre-push` git hook (on your fork), you can either comment out the contents of the lefthook.yaml file or remove it, as well as uninstall lefthook from the package.json file._ i.e + ```sh - $ rm lefthook.yaml + $ rm lefthook.yaml $ yarn remove lefthook ``` - ### Branch naming conventions +### Branch naming conventions Please follow the following branch naming scheme when creating your branch: -- `feat/foo-bar` for new features -- `fix/foo-bar` for bug fixes -- `test/foo-bar` when the change concerns only the test suite -- `refactor/foo-bar` when refactoring code without any behavior change -- `style/foo-bar` when addressing some style issue -- `docs/foo-bar` for updates to the README.md, this file, or similar documents +* `feat/foo-bar` for new features +* `fix/foo-bar` for bug fixes +* `test/foo-bar` when the change concerns only the test suite +* `refactor/foo-bar` when refactoring code without any behavior change +* `style/foo-bar` when addressing some style issue +* `docs/foo-bar` for updates to the README.md, this file, or similar documents +* `ci/foo-bar` for updates to the GitHub workflows or actions + +## Releases + +This repo uses [Semantic Release](https://github.com/semantic-release/semantic-release) for automated releases once per week. The release is triggered by a GitHub Action on the `main` branch. The release is based on the commit messages, so please follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. ## Join the Community -- Join the Kong discussions at the Kong Nation forum: [https://discuss.konghq.com/](https://discuss.konghq.com/) -- Follow us on Twitter: [https://twitter.com/thekonginc](https://twitter.com/thekonginc) -- Check out the docs: [https://docs.konghq.com/](https://docs.konghq.com/) -- Keep updated on YouTube by subscribing: [https://www.youtube.com/c/KongInc/videos](https://www.youtube.com/c/KongInc/videos) -- Read up on the latest happenings at our blog: [https://konghq.com/blog/](https://konghq.com/blog/) -- Visit our homepage to learn more: [https://konghq.com/](https://konghq.com/) +* Join the Kong discussions at the Kong Nation forum: [https://discuss.konghq.com/](https://discuss.konghq.com/) +* Follow us on Twitter: [https://twitter.com/thekonginc](https://twitter.com/thekonginc) +* Check out the docs: [https://docs.konghq.com/](https://docs.konghq.com/) +* Keep updated on YouTube by subscribing: [https://www.youtube.com/c/KongInc/videos](https://www.youtube.com/c/KongInc/videos) +* Read up on the latest happenings at our blog: [https://konghq.com/blog/](https://konghq.com/blog/) +* Visit our homepage to learn more: [https://konghq.com/](https://konghq.com/) ## License diff --git a/cypress/e2e/fixtures/consts.ts b/cypress/e2e/fixtures/consts.ts index e0f9af4b..5eedc7ab 100644 --- a/cypress/e2e/fixtures/consts.ts +++ b/cypress/e2e/fixtures/consts.ts @@ -90,4 +90,8 @@ const defaultContext: PortalContext = { allowed_time_period: '2022-03-25T13:15:02.104Z' } -export { versions, product, productVersion, productRegistration, apps, defaultContext } +const productRegistrations: GetRegistrationResponse[] = [ + productRegistration +] + +export { versions, product, productVersion, productRegistration, productRegistrations, apps, defaultContext } diff --git a/cypress/e2e/specs/api_documentation.spec.ts b/cypress/e2e/specs/api_documentation.spec.ts index 8f5b8da2..26bd14f5 100644 --- a/cypress/e2e/specs/api_documentation.spec.ts +++ b/cypress/e2e/specs/api_documentation.spec.ts @@ -4,8 +4,9 @@ import documentTreeJSON from '../fixtures/dochub_mocks/documentTree.json' describe('Api Documentation Page', () => { beforeEach(() => { + product.document_count = 1 cy.mockPrivatePortal() - cy.mockProduct(product.id) + cy.mockProduct(product.id, product) cy.mockGetProductDocumentBySlug(product.id, 'bar') cy.mockGetProductDocuments(product.id) cy.mockProductOperations() @@ -15,6 +16,16 @@ describe('Api Documentation Page', () => { const PARENT_DOCUMENT_URL = `/docs/${product.id}/${documentTreeJSON[0].slug}` const CHILD_DOCUMENT_URL = `${PARENT_DOCUMENT_URL}/${documentTreeJSON[0].children[0].slug}` + it('displays empty state when no products', () => { + product.document_count = 0 + cy.mockProduct(product.id, product) + cy.mockProductDocumentTree(product.id, { body: [] }) + cy.visit(`/docs/${product.id}`) + + cy.get('[data-testid="documentation-empty-state"]').should('be.visible') + cy.get('[data-testid="portal-document-viewer"]').should('not.exist') + }) + it('displays proper error message when 400', () => { cy.intercept( 'GET', diff --git a/cypress/e2e/specs/application_registration.spec.ts b/cypress/e2e/specs/application_registration.spec.ts index be5d9c90..90db3ab0 100644 --- a/cypress/e2e/specs/application_registration.spec.ts +++ b/cypress/e2e/specs/application_registration.spec.ts @@ -121,6 +121,7 @@ describe('Application Registration', () => { cy.mockAppearance() cy.mockStylesheetCss() cy.mockStylesheetFont() + cy.mockContextualAnalytics() }) it('displays empty dashboard for my apps', () => { @@ -229,8 +230,8 @@ describe('Application Registration', () => { mockApplicationWithCredAndReg(apps[0]) // go to application details cy.get('[data-testid="applications-table"] tbody tr') - .contains(apps[0].name) - .click() + .contains(apps[0].name) + .click() // use breadcrumb to navigate back to My Apps cy.get('.k-breadcrumbs .k-breadcrumbs-item a').contains('My Apps').click() diff --git a/cypress/e2e/specs/contextual-analytics.spec.ts b/cypress/e2e/specs/contextual-analytics.spec.ts new file mode 100644 index 00000000..cd58a9d3 --- /dev/null +++ b/cypress/e2e/specs/contextual-analytics.spec.ts @@ -0,0 +1,114 @@ +import { apps, productRegistration, productRegistrations, versions } from '../fixtures/consts' + +describe('Contextual Developer Analytics', () => { + beforeEach(() => { + cy.mockPrivatePortal() + cy.mockApplications(apps, 4) + cy.intercept('POST', '**/api/v2/stats*', { + statusCode: 200, + body: { + records: [] + }, + delay: 0 + }) + }) + + const selectors = { + chartsParent: '[data-testid="analytics-charts"]', + dashboardDropdownLink: '[data-testid="dropdown-analytics-dashboard"]', + dateTimePicker: '[data-testid="analytics-timepicker"]', + metricCardsParent: '[data-testid="analytics-metric-cards"]', + viewAnalyticsButton: '[data-testid="application-dashboard-button"]' + } + + it('My Apps – displays displays metric cards if the feature flag is on', () => { + cy.mockLaunchDarklyFlags([{ name: 'ma-1002-dev-portal-contextual-analytics', value: true }]) + + cy.mockApplications(apps, 4) + + cy.visit('/', { useOriginalFn: true }) + cy.visit('/my-apps') + + cy.get(selectors.metricCardsParent).should('exist') + cy.get(selectors.metricCardsParent).find('.metricscard').should('have.length', 3) + + cy.get('[data-testid="applications-table"]').find('.actions-badge').first().click() + cy.get(selectors.dashboardDropdownLink).should('exist') + }) + + it('My Apps – does not display metric cards or the analytics dropdown link if the feature flag is off', () => { + cy.mockLaunchDarklyFlags([{ name: 'ma-1002-dev-portal-contextual-analytics', value: false }]) + + cy.mockApplications(apps, 5) + cy.visit('/my-apps') + + cy.get(selectors.metricCardsParent).should('not.exist') + cy.get('[data-testid="applications-table"]').find('.actions-badge').first().click() + cy.get(selectors.dashboardDropdownLink).should('not.exist') + }) + + it('My App details page – does not display Metrics Card, View Analytics button if the feature flag is off', () => { + cy.mockLaunchDarklyFlags([{ name: 'ma-1002-dev-portal-contextual-analytics', value: true }]) + cy.mockApplications(apps, 4) + + cy.intercept( + 'GET', + `**/api/v2/applications/${apps[0].id}`, { + statusCode: 200, + body: { ...apps[0] } + } + ).as('getSingleApplication') + + cy.mockApplicationWithCredAndReg(apps[0]) + + cy.visit(`/application/${apps[0].id}`) + cy.get('[data-testid="analytics-metric-cards"]').should('not.exist') + cy.get('[data-testid="application-dashboard-button"]').should('not.exist') + }) + + it('App Dashboard - vitals elements load when contextual analytics feature flag is on', () => { + cy.mockLaunchDarklyFlags([{ name: 'ma-1002-dev-portal-contextual-analytics', value: true }]) + cy.mockApplications(apps, 4) + + cy.intercept('GET', `**/api/v2/applications/${apps[0].id}`, { + statusCode: 200, + body: apps[0], + delay: 0 + }).as('getSingleApplication') + + cy.intercept( + 'GET', + `**/api/v2/applications/${apps[0].id}/registrations*`, + { + body: { + data: productRegistrations, + meta: { + page: { + total: 1, + number: 1, + size: 1 + } + } + }, + delay: 0 + } + ).as('getApplicationRegistration') + + cy.visit('/my-apps') + + // Navigate to Application Dashboard page + cy.get('[data-testid="applications-table"]').find('.actions-badge').first().click() + cy.get(selectors.dashboardDropdownLink).first().click() + + // All application dashboard elements should be present + cy.get('.analytics-filters').should('exist') + cy.get(selectors.metricCardsParent).should('exist') + cy.get(selectors.chartsParent).should('exist') + + // Check that the Service Versions filter bar contains at least one item + const mockedServiceVersionName = `${productRegistrations[0].product_name} - ${productRegistrations[0].product_version_name}` + + cy.get('[data-testid="k-multiselect-input"]').should('exist').click() + cy.get('.k-multiselect-item').first().should('contain', mockedServiceVersionName) + }) +}) diff --git a/cypress/e2e/support/index.ts b/cypress/e2e/support/index.ts index edb6d5d5..04e08ee7 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, PortalAppearance, PortalContext, Product, ProductCatalogIndexSource, ProductVersion, ProductVersionSpecOperationsOperationsInner } from '@kong/sdk-portal-js' +import { GetApplicationResponse, GetRegistrationResponse, ListCredentialsResponseDataInner, PortalAppearance, PortalContext, Product, ProductCatalogIndexSource, ProductVersion, ProductVersionSpecOperationsOperationsInner } from '@kong/sdk-portal-js' import './mock-commands' import { SinonStub } from 'cypress/types/sinon' @@ -25,6 +25,8 @@ declare global { mockProductApiDocument(productId?: string, options?: Partial & {body: any}): Chainable> mockProduct(productId?: string, mockProduct?: Product, mockVersions?: ProductVersion[]): Chainable> mockApplications(searchResults?: 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> mockProductVersionApplicationRegistration(value:any): Chainable> mockProductsCatalog(count?: number, overrides?: Partial[], pageNum?:number, pageSize?:number): Chainable> diff --git a/cypress/e2e/support/mock-commands.ts b/cypress/e2e/support/mock-commands.ts index 792e8e41..6a9a7c99 100644 --- a/cypress/e2e/support/mock-commands.ts +++ b/cypress/e2e/support/mock-commands.ts @@ -7,7 +7,23 @@ import document from '../fixtures/dochub_mocks/document.json' import documentTreeJson from '../fixtures/dochub_mocks/documentTree.json' import apiDocumentationJson from '../fixtures/dochub_mocks/parentApiDocumentation.json' import petstoreOperationsV2 from '../fixtures/v2/petstoreOperations.json' -import { ListApplicationsResponse, ListDocumentsTree, PortalAppearance, PortalContext, Product, ProductDocument, ProductDocumentRaw, ProductVersionListPage, ProductVersionSpec, ProductVersionSpecOperations, ProductVersionSpecOperationsOperationsInner, SearchResults } from '@kong/sdk-portal-js' +import { + GetApplicationResponse, + ListApplicationsResponse, + ListCredentialsResponse, + ListDocumentsTree, + ListRegistrationsResponse, + PortalAppearance, + PortalContext, + Product, + ProductDocument, + ProductDocumentRaw, + ProductVersionListPage, + ProductVersionSpecDocument, + ProductVersionSpecOperations, + ProductVersionSpecOperationsOperationsInner, + SearchResults +} from '@kong/sdk-portal-js' import { THEMES } from '../fixtures/theme.constant' Cypress.Commands.add('mockStylesheetCss', (theme = 'mint_rocket', fonts = { @@ -331,6 +347,60 @@ Cypress.Commands.add('mockRegistrations', (applicationId = '*', registrations = }).as('getRegistrations') }) +Cypress.Commands.add('mockApplicationWithCredAndReg', ( + data: GetApplicationResponse, + credentials = [], + registrations = [] +) => { + const applicationResponse: GetApplicationResponse = data + + cy.intercept('GET', `**/api/v2/applications/${data.id}`, { + statusCode: 200, + body: applicationResponse + }).as('getApplication') + + const credsResponse: ListCredentialsResponse = { + data: credentials, + meta: { + page: { + total: credentials.length, + size: 10, + number: 1 + } + } + } + + cy.intercept('GET', `**/api/v2/applications/${data.id}/credentials*`, { + statusCode: 200, + body: credsResponse + }).as('getApplicationCredentials') + + const registrationsResponse: ListRegistrationsResponse = { + data: registrations, + meta: { + page: { + total: registrations.length, + size: 10, + number: 1 + } + } + } + + cy.intercept('GET', `**/api/v2/applications/${data.id}/registrations*`, { + statusCode: 200, + body: registrationsResponse + }).as('getApplicationRegistrations') +}) + +Cypress.Commands.add('mockContextualAnalytics', () => { + return cy.intercept( + 'POST', '**/api/v2/stats', { + statusCode: 200, + body: { records: [] }, + delay: 0 + }).as('getContextualAnalytics') +}) + Cypress.Commands.add('mockProductVersionApplicationRegistration', (version, config = {}) => { return cy.intercept( 'GET', @@ -436,7 +506,7 @@ Cypress.Commands.add('mockGetProductDocumentTree', (productId) => { }) Cypress.Commands.add('mockProductVersionSpec', (productId = '*', versionId = '*', content = JSON.stringify(petstoreJson30)) => { - const specResponse: ProductVersionSpec = { + const specResponse: ProductVersionSpecDocument = { api_type: 'openapi', content } diff --git a/lefthook.yaml b/lefthook.yaml index 3d76ac25..fb87e800 100644 --- a/lefthook.yaml +++ b/lefthook.yaml @@ -14,12 +14,3 @@ pre-push: - merge - rebase run: ./.github/scripts/pin-version - -commit-msg: - parallel: true - commands: - commitlint: - skip: - - merge - - rebase - run: yarn commitlint --edit diff --git a/package.json b/package.json index 9f24983a..6b864389 100644 --- a/package.json +++ b/package.json @@ -18,18 +18,22 @@ "test:e2e": "concurrently --kill-others \"yarn build:watch\" \"cypress open -C cypress.config.js --e2e -b chrome\" \"node cypress.server.mjs \"", "test:e2e:ci": "concurrently --success first --kill-others \"yarn build && DEBUG=cypress:server cypress run -C cypress.config.js --e2e -b chrome\" \"node cypress.server.mjs \"", "typecheck": "vue-tsc -p './tsconfig.json' --noEmit", - "typecheck:tests": "vue-tsc -p './cypress/e2e/tsconfig.json' --noEmit" + "typecheck:tests": "vue-tsc -p './cypress/e2e/tsconfig.json' --noEmit", + "semantic-release": "semantic-release" }, "optionalDependencies": { "launchdarkly-js-client-sdk": "3.1.3" }, "dependencies": { - "@kong-ui-public/copy-uuid": "0.3.15", - "@kong-ui-public/document-viewer": "0.9.0", - "@kong-ui-public/spec-renderer": "0.10.0", - "@kong/kong-auth-elements": "2.1.0", - "@kong/kongponents": "8.98.0", - "@kong/sdk-portal-js": "1.0.0", + "@kong-ui-public/analytics-chart": "0.8.25", + "@kong-ui-public/analytics-metric-provider": "1.1.17", + "@kong-ui-public/analytics-utilities": "0.5.3", + "@kong-ui-public/copy-uuid": "1.1.5", + "@kong-ui-public/document-viewer": "0.10.5", + "@kong-ui-public/spec-renderer": "0.11.6", + "@kong/kong-auth-elements": "2.8.0", + "@kong/kongponents": "8.116.2", + "@kong/sdk-portal-js": "2.1.0", "@xstate/vue": "2.0.0", "axios": "0.27.2", "date-fns": "2.30.0", @@ -43,8 +47,10 @@ "devDependencies": { "@commitlint/cli": "17.6.3", "@commitlint/config-conventional": "17.6.5", + "@semantic-release/changelog": "6.0.3", + "@semantic-release/git": "10.0.1", "@typescript-eslint/eslint-plugin": "5.62.0", - "@typescript-eslint/parser": "5.61.0", + "@typescript-eslint/parser": "5.62.0", "@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue-jsx": "3.0.1", "@vue/compiler-sfc": "3.3.4", @@ -57,19 +63,21 @@ "cypress-split": "1.3.8", "cypress-terminal-report": "5.3.2", "cz-conventional-changelog": "3.3.0", - "eslint": "8.45.0", + "druid.d.ts": "0.12.2", + "eslint": "8.46.0", "eslint-plugin-cypress": "2.13.3", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "2.28.0", "eslint-plugin-node": "11.1.0", "eslint-plugin-portal-vue": "file:./eslint-plugin-portal-vue", "eslint-plugin-promise": "6.1.1", "eslint-plugin-standard": "5.0.0", - "eslint-plugin-vue": "9.15.0", + "eslint-plugin-vue": "9.16.1", "express": "4.18.2", "lefthook": "1.4.1", "openapi-types": "12.1.3", "rollup-plugin-visualizer": "5.9.2", "sass": "1.64.0", + "semantic-release": "21.0.7", "tailwindcss": "3.3.2", "typescript": "4.9.5", "vite": "4.4", @@ -89,7 +97,7 @@ "engines": { "npm": "please-use-yarn", "node": ">=18.16.0 < 19", - "yarn": "^1.22.19" + "yarn": ">=1.22.19" }, "volta": { "node": "18.16.0", diff --git a/src/assets/mixins.scss b/src/assets/mixins.scss new file mode 100644 index 00000000..39b6ebdc --- /dev/null +++ b/src/assets/mixins.scss @@ -0,0 +1,8 @@ +// Mixins + +@mixin grid-columns($columns) { + display: grid; + grid-template-columns: repeat($columns, 1fr); + grid-column-gap: 16px; + grid-row-gap: 16px; +} \ No newline at end of file diff --git a/src/assets/variables.scss b/src/assets/variables.scss new file mode 100644 index 00000000..b4bbb8f4 --- /dev/null +++ b/src/assets/variables.scss @@ -0,0 +1,6 @@ +// Breakpoints +$viewport-sm: 640px; +$viewport-md: 768px; +$viewport-lg: 1024px; // Mobile layout breakpoint +$viewport-xl: 1280px; +$viewport-2xl: 1536px; diff --git a/src/components/Catalog.vue b/src/components/Catalog.vue index 2ebb5a24..5464e8f4 100644 --- a/src/components/Catalog.vue +++ b/src/components/Catalog.vue @@ -41,6 +41,7 @@ diff --git a/src/components/CatalogTableList.vue b/src/components/CatalogTableList.vue index 27f471d6..cf28929c 100644 --- a/src/components/CatalogTableList.vue +++ b/src/components/CatalogTableList.vue @@ -6,6 +6,7 @@ has-side-border :headers="tableHeaders" is-small + :is-loading="loading" is-clickable disable-pagination @row:click="handleRowClick" @@ -55,6 +56,10 @@ export default defineComponent({ products: { type: Array as PropType, default: () => [] + }, + loading: { + type: Boolean, + default: false } }, setup (props) { diff --git a/src/components/product/Sidebar.vue b/src/components/product/Sidebar.vue index 7760ce44..bff5757a 100644 --- a/src/components/product/Sidebar.vue +++ b/src/components/product/Sidebar.vue @@ -5,6 +5,12 @@ {{ product?.name }} + ) => { app.component('Content', Content) app.component('EmptyState', EmptyState) + app.component('AnalyticsEmptyState', AnalyticsEmptyState) } diff --git a/src/components/vitals/AnalyticsEmptyState.vue b/src/components/vitals/AnalyticsEmptyState.vue new file mode 100644 index 00000000..1c92bfd0 --- /dev/null +++ b/src/components/vitals/AnalyticsEmptyState.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/components/vitals/AnalyticsMetricsCard.vue b/src/components/vitals/AnalyticsMetricsCard.vue new file mode 100644 index 00000000..e15d1061 --- /dev/null +++ b/src/components/vitals/AnalyticsMetricsCard.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/components/vitals/ChartPanel.vue b/src/components/vitals/ChartPanel.vue new file mode 100644 index 00000000..0d09a4e1 --- /dev/null +++ b/src/components/vitals/ChartPanel.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/src/components/vitals/MetricsProvider.vue b/src/components/vitals/MetricsProvider.vue new file mode 100644 index 00000000..ad67f306 --- /dev/null +++ b/src/components/vitals/MetricsProvider.vue @@ -0,0 +1,82 @@ + + diff --git a/src/composables/useAllowedTimeframes.ts b/src/composables/useAllowedTimeframes.ts new file mode 100644 index 00000000..543d19d1 --- /dev/null +++ b/src/composables/useAllowedTimeframes.ts @@ -0,0 +1,60 @@ +import { computed } from 'vue' +import { + TimeframeKeys, + TimePeriods, + timeframeToDatepickerTimeperiod +} from '@kong-ui-public/analytics-utilities' +import { PortalTimeframeKeys } from '@/types/vitals' +import { useI18nStore } from '@/stores' + +const helpText = useI18nStore().state.helpText + +export default function useAllowedTimeframes (allowedTimePeriod) { + const now = computed(() => new Date().getTime()) + const dateOffset = allowedTimePeriod.value === PortalTimeframeKeys.NINETY_DAYS + ? TimePeriods.get(TimeframeKeys.THIRTY_DAY).timeframeLengthMs() * 3 // 90 days + : TimePeriods.get(TimeframeKeys.ONE_DAY).timeframeLengthMs() + + const allowedTimePeriods = computed(() => { + // Default to showing all possible timeframes + const sections = [ + { + section: helpText.analytics.sectionLast, + values: [ + TimePeriods.get(TimeframeKeys.FIFTEEN_MIN), + TimePeriods.get(TimeframeKeys.ONE_HOUR), + TimePeriods.get(TimeframeKeys.SIX_HOUR), + TimePeriods.get(TimeframeKeys.TWELVE_HOUR), + TimePeriods.get(TimeframeKeys.ONE_DAY), + TimePeriods.get(TimeframeKeys.SEVEN_DAY), + TimePeriods.get(TimeframeKeys.THIRTY_DAY) + ].filter((val) => val.timeframeLengthMs() <= dateOffset).map(timeframeToDatepickerTimeperiod) + }, + + // For "Current" and "Previous" sections, we check for less than because we want to ensure neither of these + // section show up. + { + section: helpText.analytics.sectionCurrent, + values: [ + TimePeriods.get(TimeframeKeys.CURRENT_WEEK), + TimePeriods.get(TimeframeKeys.CURRENT_MONTH) + ].filter((val) => val.timeframeLengthMs() < dateOffset).map(timeframeToDatepickerTimeperiod) + }, + { + section: helpText.analytics.sectionPrevious, + values: [ + TimePeriods.get(TimeframeKeys.PREVIOUS_WEEK), + TimePeriods.get(TimeframeKeys.PREVIOUS_MONTH) + ].filter((val) => val.timeframeLengthMs() < dateOffset).map(timeframeToDatepickerTimeperiod) + } + ] + + // Strip out Sections that do not contain at least one Timeframe + return sections.filter(s => s.values.length !== 0) + }) + + return { + timePeriods: allowedTimePeriods, + minDateCalendar: new Date(now.value - dateOffset) + } +} diff --git a/src/composables/useChartQueryBuilder.ts b/src/composables/useChartQueryBuilder.ts new file mode 100644 index 00000000..4e61e267 --- /dev/null +++ b/src/composables/useChartQueryBuilder.ts @@ -0,0 +1,26 @@ +import cloneDeep from 'lodash.clonedeep' +import type { ProductVersion } from '@/types/vitals' + +export default function useChartQueryBuilder (baseQuery, appId: string, productVersions: Array) { + const query = cloneDeep(baseQuery) + + // Append as single Application ID to filter + if (appId) { + query.filter = [...query.filter, { + type: 'IN', + dimension: 'APPLICATION', + values: [appId] + }] + } + + // Filter further by Product Versions, if any have been selected in dropdown + if (productVersions.length) { + query.filter = [...query.filter, { + type: 'IN', + dimension: 'API_PRODUCT_VERSION', + values: productVersions.map(entry => entry.value) + }] + } + + return query +} diff --git a/src/composables/useChartRequest.ts b/src/composables/useChartRequest.ts new file mode 100644 index 00000000..f265b21d --- /dev/null +++ b/src/composables/useChartRequest.ts @@ -0,0 +1,52 @@ +import { computed } from 'vue' +import { + ApplicationAnalyticsApiQueryApplicationAnalyticsRequest, + QueryApplicationAnalytics200Response +} from '@kong/sdk-portal-js' +import usePortalApi from '@/hooks/usePortalApi' +import { snakeToCamelCase } from '@/helpers/snakeToCamelCase' +const { portalApiV2 } = usePortalApi() + +export default async function useChartRequest (query, timeseriesQueryTime): Promise { + const startMs = computed(() => timeseriesQueryTime.startMs()) + const endMs = computed(() => timeseriesQueryTime.endMs()) + const granularity = computed(() => timeseriesQueryTime.granularityMs()) + + const vitalsRequest: ApplicationAnalyticsApiQueryApplicationAnalyticsRequest = { + queryApplicationAnalytics: { + start_ms: Number(startMs.value), + end_ms: Number(endMs.value), + granularity_ms: granularity.value, + // Append `dimensions`, `metrics`, `filter`, and `meta` + ...query + } + } + + if (!query.filter || !query.filter.length) { + return null + } + + try { + const res = await portalApiV2.value.service.applicationAnalyticsApi.queryApplicationAnalytics(vitalsRequest) + + const result = { + meta: snakeToCamelCase(res?.data?.meta), + records: res?.data?.records + } as QueryApplicationAnalytics200Response + + return result + } catch (err) { + console.error(err) + + return { + data: { + records: [], + meta: { + start_ms: startMs, + end_ms: endMs + } + }, + status: '500' + } as QueryApplicationAnalytics200Response + } +} diff --git a/src/constants/chartQueries.ts b/src/constants/chartQueries.ts new file mode 100644 index 00000000..b769613c --- /dev/null +++ b/src/constants/chartQueries.ts @@ -0,0 +1,73 @@ +import { Filter } from 'druid.d.ts' + +interface QueryMeta { + query_id: string + start?: number + end?: number +} + +interface DruidQuery { + dimensions: string[] + metrics: string[] + meta: QueryMeta + filter?: Filter[] + granularity?: number +} + +export const chartQueryTrafficRequests: DruidQuery = { + dimensions: ['TIME', 'API_PRODUCT_VERSION'], + metrics: ['REQUEST_COUNT'], + meta: { query_id: 'portal-chart-traffic' }, + filter: [] +} + +export const chartQueryTrafficLatency: DruidQuery = { + dimensions: ['TIME', 'API_PRODUCT_VERSION'], + metrics: ['RESPONSE_LATENCY_P99'], + meta: { query_id: 'portal-chart-latency' }, + filter: [] +} + +export const chartQueryProductVersions4xx: DruidQuery = { + dimensions: ['TIME', 'API_PRODUCT_VERSION'], + metrics: ['REQUEST_COUNT'], + meta: { query_id: 'portal-4xx-by-product-version' }, + filter: [{ + type: 'IN', + dimension: 'STATUS_CODE_GROUPED', + values: ['4XX'] + }] +} + +export const chartQueryProductVersions5xx: DruidQuery = { + dimensions: ['TIME', 'API_PRODUCT_VERSION'], + metrics: ['REQUEST_COUNT'], + meta: { query_id: 'portal-5xx-by-product-version' }, + filter: [{ + type: 'IN', + dimension: 'STATUS_CODE_GROUPED', + values: ['5XX'] + }] +} + +export const chartQueryStatusCode4xx: DruidQuery = { + dimensions: ['STATUS_CODE'], + metrics: ['REQUEST_COUNT'], + meta: { query_id: 'portal-4xx-by-status-code' }, + filter: [{ + type: 'IN', + dimension: 'STATUS_CODE_GROUPED', + values: ['4XX'] + }] +} + +export const chartQueryStatusCode5xx: DruidQuery = { + dimensions: ['STATUS_CODE'], + metrics: ['REQUEST_COUNT'], + meta: { query_id: 'portal-5xx-by-status-code' }, + filter: [{ + type: 'IN', + dimension: 'STATUS_CODE_GROUPED', + values: ['5XX'] + }] +} diff --git a/src/constants/feature-flags.ts b/src/constants/feature-flags.ts index f4eef658..811115af 100644 --- a/src/constants/feature-flags.ts +++ b/src/constants/feature-flags.ts @@ -1,2 +1,3 @@ export enum FeatureFlags { + PortalContextualAnalytics = 'ma-1002-dev-portal-contextual-analytics' } diff --git a/src/helpers/snakeToCamelCase.ts b/src/helpers/snakeToCamelCase.ts new file mode 100644 index 00000000..3c5b9b01 --- /dev/null +++ b/src/helpers/snakeToCamelCase.ts @@ -0,0 +1,17 @@ +export const snakeToCamelCase = (obj) => { + if (typeof obj !== 'object' || obj === null) { return obj } + + if (Array.isArray(obj)) { + return obj.map(item => snakeToCamelCase(item)) + } + + return Object.keys(obj).reduce((acc, key) => { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const camelKey = key.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase()) + + acc[camelKey] = snakeToCamelCase(obj[key]) + } + + return acc + }, {}) +} diff --git a/src/locales/ca_ES.ts b/src/locales/ca_ES.ts index fce188af..39a35a2e 100644 --- a/src/locales/ca_ES.ts +++ b/src/locales/ca_ES.ts @@ -37,7 +37,10 @@ export const ca_ES: I18nType = { }, productVersion: { deprecatedWarningProduct: "Aquesta versió del producte ja no està vigent. Els punts d'interacció seguiran sent totalment funcionals fins que aquesta versió sigui retirada.", - unableToRetrieveDoc: 'No es pot recuperar la documentació' + unableToRetrieveDoc: 'No es pot recuperar la documentació', + noProductVersionsDetail: 'This has not been translated', + noProductVersionsTitle: 'This has not been translated', + registerProductVersion: 'This has not been translated' }, userDropdown: { myApps: 'Les meves aplicacions', @@ -121,6 +124,32 @@ export const ca_ES: I18nType = { headerDescription3: 'una vegada.', headerDescription4: 'Copieu aquest valor i guardeu-lo per a les vostres referències.' }, + analytics: { + filterLabelProductVersions: 'This has not been translated', + chartOverview: 'This has not been translated', + chartTitleRequests: 'This has not been translated', + chartTitleLatency: 'This has not been translated', + chartTitle4xxProductVersion: 'This has not been translated', + chartTitle5xxProductVersion: 'This has not been translated', + chartTitle4xxStatusCode: 'This has not been translated', + chartTitle5xxStatusCode: 'This has not been translated', + dashboard: 'This has not been translated', + resultsLimited: 'This has not been translated', + notAvailable: 'This has not been translated', + sectionCurrent: 'This has not been translated', + sectionLast: 'This has not been translated', + sectionPrevious: 'This has not been translated', + selectDateRange: 'This has not been translated', + selectProductVersions: 'This has not been translated', + summary: 'This has not been translated', + summary24Hours: 'This has not been translated', + summary30Days: 'This has not been translated', + summaryTooltip: (timespan: string) => `This ${timespan} has not been translated`, + timeRange: 'This has not been translated', + totalRequests: 'This has not been translated', + unableToFetch: (itemName: string) => `This has not been translaed ${itemName}`, + viewAnalytics: 'This has not been translateds' + }, productList: { titleProducts: 'Productes', actions: { @@ -175,6 +204,8 @@ export const ca_ES: I18nType = { isEmail: "L'adreça de correu electrònic ha de ser una adreça vàlida" }, apiDocumentation: { + emptyTitle: 'This has not been translated', + emptyMessage: 'This has not been translated', error: { description: "S'ha produït un error inesperat en carregar el document sol·licitat.Si us plau, torneu- ho a provar més tard", linkText: 'Tornar a la pàgina inicial →' @@ -187,6 +218,7 @@ export const ca_ES: I18nType = { linkText: 'Tornar a la pàgina inicial →' }, sidebar: { + noVersions: 'This has not been translated', deprecated: ' (Desactivat)', noResultsProduct: 'Sense versions de producte' }, diff --git a/src/locales/de.ts b/src/locales/de.ts index 1332f9a5..c893b1c4 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -37,7 +37,10 @@ export const de: I18nType = { }, productVersion: { deprecatedWarningProduct: 'Diese Produktversion ist veraltet. Die Endpunkte bleiben voll funktionsfähig, bis diese Version eingestellt wird.', - unableToRetrieveDoc: 'Keine Dokumentation verfügbar' + unableToRetrieveDoc: 'Keine Dokumentation verfügbar', + noProductVersionsDetail: 'This has not been translated', + noProductVersionsTitle: 'This has not been translated', + registerProductVersion: 'This has not been translated' }, userDropdown: { myApps: 'Meine Applikationen', @@ -121,6 +124,32 @@ export const de: I18nType = { headerDescription3: 'nur einmal angezeigt.', headerDescription4: 'Bitte kopieren und an einem sicheren Ort speichern.' }, + analytics: { + filterLabelProductVersions: 'This has not been translated', + chartOverview: 'This has not been translated', + chartTitleRequests: 'This has not been translated', + chartTitleLatency: 'This has not been translated', + chartTitle4xxProductVersion: 'This has not been translated', + chartTitle5xxProductVersion: 'This has not been translated', + chartTitle4xxStatusCode: 'This has not been translated', + chartTitle5xxStatusCode: 'This has not been translated', + dashboard: 'This has not been translated', + resultsLimited: 'This has not been translated', + notAvailable: 'This has not been translated', + sectionCurrent: 'This has not been translated', + sectionLast: 'This has not been translated', + sectionPrevious: 'This has not been translated', + selectDateRange: 'This has not been translated', + selectProductVersions: 'This has not been translated', + summary: 'This has not been translated', + summary24Hours: 'This has not been translated', + summary30Days: 'This has not been translated', + summaryTooltip: (timespan: string) => `This ${timespan} has not been translated`, + timeRange: 'This has not been translated', + totalRequests: 'This has not been translated', + unableToFetch: (itemName: string) => `This has not been translaed ${itemName}`, + viewAnalytics: 'This has not been translateds' + }, productList: { titleProducts: 'Produkte', actions: { @@ -175,6 +204,8 @@ export const de: I18nType = { isEmail: 'E-Mail ist ungültig' }, apiDocumentation: { + emptyTitle: 'This has not been translated', + emptyMessage: 'This has not been translated', error: { description: 'Ein unerwarteter Fehler ist aufgetreten, als versucht wurde, das angeforderte Dokument zu laden. Bitte versuchen Sie es später noch einmal', linkText: 'Zurück zum Start →' @@ -187,6 +218,7 @@ export const de: I18nType = { linkText: 'Zurück zum Start →' }, sidebar: { + noVersions: 'This has not been translated', deprecated: ' (Veraltet)', noResultsProduct: 'Keine Produktversionen' }, diff --git a/src/locales/en.ts b/src/locales/en.ts index b0c866e9..747f21a6 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -35,7 +35,10 @@ export const en = { }, productVersion: { deprecatedWarningProduct: 'This product version is now deprecated. The endpoints will remain fully usable until this version is sunsetted.', - unableToRetrieveDoc: 'Unable to retrieve documentation' + unableToRetrieveDoc: 'Unable to retrieve documentation', + noProductVersionsDetail: 'This App is not registered for any Product Versions', + noProductVersionsTitle: 'No Product Versions', + registerProductVersion: 'Register Product version' }, userDropdown: { myApps: 'My Apps', @@ -119,6 +122,32 @@ export const en = { headerDescription3: 'only be shown once.', headerDescription4: 'Please copy this value and keep for your records.' }, + analytics: { + filterLabelProductVersions: 'Product Versions', + chartOverview: 'Chart Overview', + chartTitleRequests: 'Requests by Product Version', + chartTitleLatency: 'P99 Latency by Product Version', + chartTitle4xxProductVersion: '4xx by Product Version', + chartTitle5xxProductVersion: '5xx by Product Version', + chartTitle4xxStatusCode: '4xx by Status Code', + chartTitle5xxStatusCode: '5xx by Status Code', + dashboard: 'Dashboard', + resultsLimited: 'Not all results shown. Refine your search for more related results.', + notAvailable: 'Not available', + sectionCurrent: 'Current', + sectionLast: 'Last', + sectionPrevious: 'Previous', + selectDateRange: 'Please select a date range', + selectProductVersions: 'Select Product Versions', + summary: 'Summary', + summary24Hours: '24 Hours', + summary30Days: '30 Days', + summaryTooltip: (timespan: string) => `Showing ${timespan} summary of analytics for all apps`, + timeRange: 'Time Range', + totalRequests: 'Total Requests', + unableToFetch: (itemName: string) => `Unable to fetch ${itemName}`, + viewAnalytics: 'View analytics' + }, productList: { titleProducts: 'Products', actions: { @@ -173,6 +202,8 @@ export const en = { isEmail: 'Email must be a valid email address' }, apiDocumentation: { + emptyTitle: 'No Documentation', + emptyMessage: 'This product currently has no documentation. Reach out to your Developer Portal administrator if this is not expected.', error: { description: 'An unexpected error occurred when trying to load the requested document. Please try again later', linkText: 'Go back home →' @@ -185,6 +216,7 @@ export const en = { linkText: 'Go back home →' }, sidebar: { + noVersions: 'This product has no published product versions', deprecated: ' (Deprecated)', noResultsProduct: 'No product versions' }, diff --git a/src/locales/es_ES.ts b/src/locales/es_ES.ts index 824b116c..499d7234 100644 --- a/src/locales/es_ES.ts +++ b/src/locales/es_ES.ts @@ -37,7 +37,10 @@ export const es_ES: I18nType = { }, productVersion: { deprecatedWarningProduct: 'Esta versión del producto está obsoleta. La interfaz seguirá siendo totalmente utilizable hasta que esta versión se elimine.', - unableToRetrieveDoc: 'No se puede recuperar la documentación' + unableToRetrieveDoc: 'No se puede recuperar la documentación', + noProductVersionsDetail: 'This has not been translated', + noProductVersionsTitle: 'This has not been translated', + registerProductVersion: 'This has not been translated' }, userDropdown: { myApps: 'Mis aplicaciones', @@ -121,6 +124,32 @@ export const es_ES: I18nType = { headerDescription3: 'mostrada solamente una vez.', headerDescription4: 'Por favor, asegúrate de copiar este valor y guardarlo en un lugar seguro.' }, + analytics: { + filterLabelProductVersions: 'This has not been translated', + chartOverview: 'This has not been translated', + chartTitleRequests: 'This has not been translated', + chartTitleLatency: 'This has not been translated', + chartTitle4xxProductVersion: 'This has not been translated', + chartTitle5xxProductVersion: 'This has not been translated', + chartTitle4xxStatusCode: 'This has not been translated', + chartTitle5xxStatusCode: 'This has not been translated', + dashboard: 'This has not been translated', + resultsLimited: 'This has not been translated', + notAvailable: 'This has not been translated', + sectionCurrent: 'This has not been translated', + sectionLast: 'This has not been translated', + sectionPrevious: 'This has not been translated', + selectDateRange: 'This has not been translated', + selectProductVersions: 'This has not been translated', + summary: 'This has not been translated', + summary24Hours: 'This has not been translated', + summary30Days: 'This has not been translated', + summaryTooltip: (timespan: string) => `This ${timespan} has not been translated`, + timeRange: 'This has not been translated', + totalRequests: 'This has not been translated', + unableToFetch: (itemName: string) => `This has not been translaed ${itemName}`, + viewAnalytics: 'This has not been translated' + }, productList: { titleProducts: 'Productos', actions: { @@ -175,6 +204,8 @@ export const es_ES: I18nType = { isEmail: 'El correo electrónico debe ser una dirección de correo electrónico válida' }, apiDocumentation: { + emptyTitle: 'This has not been translated', + emptyMessage: 'This has not been translated', error: { description: 'Ocurrió un error inesperado al intentar cargar el documento. Por favor, inténtalo de nuevo más tarde', linkText: 'Regresar al inicio →' @@ -187,6 +218,7 @@ export const es_ES: I18nType = { linkText: 'Regresar al inicio →' }, sidebar: { + noVersions: 'This has not been translated', deprecated: ' (Obsoleto)', noResultsProduct: 'No hay versiones del producto' }, diff --git a/src/locales/fr.ts b/src/locales/fr.ts index c1211350..614cc14f 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -37,7 +37,10 @@ export const fr: I18nType = { }, productVersion: { deprecatedWarningProduct: 'Cette version du produit est maintenant obsolète. Les points d\'accès resteront entièrement utilisables jusqu\'à la fin de cette version.', - unableToRetrieveDoc: 'Impossible de récupérer la documentation' + unableToRetrieveDoc: 'Impossible de récupérer la documentation', + noProductVersionsDetail: 'This has not been translated', + noProductVersionsTitle: 'This has not been translated', + registerProductVersion: 'This has not been translated' }, userDropdown: { myApps: 'Mes Applications', @@ -121,6 +124,32 @@ export const fr: I18nType = { headerDescription3: 'affiché qu\'une seule fois.', headerDescription4: 'Veuillez copier cette valeur et la conserver dans vos archives.' }, + analytics: { + filterLabelProductVersions: 'This has not been translated', + chartOverview: 'This has not been translated', + chartTitleRequests: 'This has not been translated', + chartTitleLatency: 'This has not been translated', + chartTitle4xxProductVersion: 'This has not been translated', + chartTitle5xxProductVersion: 'This has not been translated', + chartTitle4xxStatusCode: 'This has not been translated', + chartTitle5xxStatusCode: 'This has not been translated', + dashboard: 'This has not been translated', + resultsLimited: 'This has not been translated', + notAvailable: 'This has not been translated', + sectionCurrent: 'This has not been translated', + sectionLast: 'This has not been translated', + sectionPrevious: 'This has not been translated', + selectDateRange: 'This has not been translated', + selectProductVersions: 'This has not been translated', + summary: 'This has not been translated', + summary24Hours: 'This has not been translated', + summary30Days: 'This has not been translated', + summaryTooltip: (timespan: string) => `This ${timespan} has not been translated`, + timeRange: 'This has not been translated', + totalRequests: 'This has not been translated', + unableToFetch: (itemName: string) => `This has not been translaed ${itemName}`, + viewAnalytics: 'This has not been translateds' + }, productList: { titleProducts: 'Produits', actions: { @@ -175,6 +204,8 @@ export const fr: I18nType = { isEmail: 'L\'adresse e-mail doit être valide' }, apiDocumentation: { + emptyTitle: 'This has not been translated', + emptyMessage: 'This has not been translated', error: { description: 'Une erreur inattendue s\'est produite lors du chargement du document demandé. Veuillez réessayer ultérieurement.', linkText: 'Retourner à la page d\'accueil →' @@ -187,6 +218,7 @@ export const fr: I18nType = { linkText: 'Retour à la page d\'accueil →' }, sidebar: { + noVersions: 'This has not been translated', deprecated: ' (Obsolète)', noResultsProduct: 'Aucune version de produit' }, diff --git a/src/locales/i18n-type.d.ts b/src/locales/i18n-type.d.ts index c83e06ea..6a1e7082 100644 --- a/src/locales/i18n-type.d.ts +++ b/src/locales/i18n-type.d.ts @@ -36,6 +36,9 @@ export interface I18nType { productVersion: { deprecatedWarningProduct: string; unableToRetrieveDoc: string; + noProductVersionsDetail: string; + noProductVersionsTitle: string; + registerProductVersion: string; }; userDropdown: { myApps: string; @@ -119,6 +122,32 @@ export interface I18nType { headerDescription3: string; headerDescription4: string; }; + analytics: { + filterLabelProductVersions: string, + chartOverview: string, + chartTitleRequests: string, + chartTitleLatency: string, + chartTitle4xxProductVersion: string, + chartTitle5xxProductVersion: string, + chartTitle4xxStatusCode: string, + chartTitle5xxStatusCode: string, + dashboard: string, + resultsLimited: string, + notAvailable: string, + sectionCurrent: string, + sectionLast: string, + sectionPrevious: string, + selectDateRange: string, + selectProductVersions: string, + summary: string, + summary24Hours: string, + summary30Days: string, + summaryTooltip: (timespan: string) => string, + timeRange: string, + totalRequests: string, + unableToFetch: (itemName: string) => string, + viewAnalytics: string, + }, productList: { titleProducts: string; actions: { @@ -173,6 +202,8 @@ export interface I18nType { isEmail: string; }; apiDocumentation: { + emptyTitle: string, + emptyMessage: string, error: { description: string; linkText: string; @@ -185,6 +216,7 @@ export interface I18nType { linkText: string; }; sidebar: { + noVersions: string; deprecated: string; noResultsProduct: string; }; diff --git a/src/main.ts b/src/main.ts index 5efd5fe3..56806b25 100644 --- a/src/main.ts +++ b/src/main.ts @@ -64,7 +64,8 @@ async function init () { is_public: isPublic, basic_auth_enabled: basicAuthEnabled, dcr_provider_ids: dcrProviderIds, - rbac_enabled: isRbacEnabled + rbac_enabled: isRbacEnabled, + allowed_time_period: allowedTimePeriod } = portalContext.data if (isPublic === false) { @@ -75,7 +76,7 @@ async function init () { const isDcr = Array.isArray(dcrProviderIds) && dcrProviderIds.length > 0 - setPortalData({ portalId, orgId, authClientConfig, featuresetId, featureSet, isPublic, isDcr, isRbacEnabled }) + setPortalData({ portalId, orgId, authClientConfig, featuresetId, featureSet, isPublic, isDcr, isRbacEnabled, allowedTimePeriod }) setSession(session) // Fetch session data from localStorage diff --git a/src/router/index.ts b/src/router/index.ts index e8db24ca..e03caf1c 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -149,6 +149,12 @@ export const portalRouter = () => { name: 'update-application', meta: { title: helpText.updateAppTitle }, component: () => import('../views/Applications/ApplicationForm.vue') + }, + { + path: 'application-dashboard', + name: 'application-dashboard', + meta: { title: 'Application Dashboard' }, + component: () => import('../views/Applications/ApplicationDashboard.vue') } ] } @@ -198,6 +204,10 @@ export const portalRouter = () => { // check is authenticated developer router.beforeEach(async (to, from, next) => { + if (to.meta.public) { + return next() + } + const sessionDoesExist = session.exists() // check if needed refirect after SSO login to the page to which we tried access previously diff --git a/src/services/PortalV2ApiService.ts b/src/services/PortalV2ApiService.ts index cbf3dae4..c42f1360 100644 --- a/src/services/PortalV2ApiService.ts +++ b/src/services/PortalV2ApiService.ts @@ -11,7 +11,8 @@ import { ProductsApi, RegistrationsApi, DocumentationApi, - VersionsApi + VersionsApi, + ApplicationAnalyticsApi } from '@kong/sdk-portal-js' export const ApiServiceAuthErrorReason = { @@ -40,7 +41,8 @@ export default class PortalV2ApiService { productsApi: ProductsApi, registrationsApi: RegistrationsApi, documentationApi: DocumentationApi, - versionsApi: VersionsApi + versionsApi: VersionsApi, + applicationAnalyticsApi: ApplicationAnalyticsApi } setAuthErrorCallback (authErrorCallback) { @@ -78,7 +80,8 @@ export default class PortalV2ApiService { productsApi: new ProductsApi(baseConfig, this.baseURL, this.client), registrationsApi: new RegistrationsApi(baseConfig, this.baseURL, this.client), documentationApi: new DocumentationApi(baseConfig, this.baseURL, this.client), - versionsApi: new VersionsApi(baseConfig, this.baseURL, this.client) + versionsApi: new VersionsApi(baseConfig, this.baseURL, this.client), + applicationAnalyticsApi: new ApplicationAnalyticsApi(baseConfig, this.baseURL, this.client) } this.client.interceptors.response.use(res => res, (originalErr) => { diff --git a/src/stores/app.ts b/src/stores/app.ts index 3d516ad4..ddc37420 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -1,6 +1,7 @@ import SessionCookie from '@/services/SessionCookie' import { defineStore } from 'pinia' import { ref } from 'vue' +import { PortalTimeframeKeys } from '@/types/vitals' interface PortalData { portalId: string; @@ -14,6 +15,7 @@ interface PortalData { isPublic: boolean; isDcr: boolean; isRbacEnabled: boolean; + allowedTimePeriod: string; } export const useAppStore = defineStore('app', () => { @@ -27,6 +29,7 @@ export const useAppStore = defineStore('app', () => { const developerSession = ref(null) const featuresetId = ref(null) const featureSet = ref('') + const allowedTimePeriod = ref(PortalTimeframeKeys.ONE_DAY) const authClientConfig = ref<{ basicAuthEnabled: boolean; oidcAuthEnabled: boolean; @@ -67,6 +70,10 @@ export const useAppStore = defineStore('app', () => { if (data.isPublic) { isPublic.value = data.isPublic } + + if (data.allowedTimePeriod) { + allowedTimePeriod.value = data.allowedTimePeriod + } } const setSession = (session: SessionCookie) => { @@ -84,6 +91,7 @@ export const useAppStore = defineStore('app', () => { developerSession, featuresetId, featureSet, + allowedTimePeriod, authClientConfig, logout, diff --git a/src/types/productVersion.ts b/src/types/productVersion.ts new file mode 100644 index 00000000..4fafc1f2 --- /dev/null +++ b/src/types/productVersion.ts @@ -0,0 +1,9 @@ +export interface ProductVersionData { + value: string + label: string +} + +export interface ProductVersionsResult { + results: ProductVersionData[], + hasMoreResults: boolean +} diff --git a/src/types/vitals.ts b/src/types/vitals.ts new file mode 100644 index 00000000..41ad127f --- /dev/null +++ b/src/types/vitals.ts @@ -0,0 +1,19 @@ +import type { Timeframe } from '@kong-ui-public/analytics-utilities' +import { Ref } from 'vue' + +export interface ProductVersion { + label: string + key: string + selected: boolean + value: string +} + +export enum PortalTimeframeKeys { + NINETY_DAYS = '90d', + ONE_DAY = '24h' +} + +export interface ChartFilters { + timeframe: Ref + apiVersions: Ref> +} diff --git a/src/views/ApiDocumentationPage.vue b/src/views/ApiDocumentationPage.vue index 65614cb0..78323d42 100644 --- a/src/views/ApiDocumentationPage.vue +++ b/src/views/ApiDocumentationPage.vue @@ -5,7 +5,22 @@ data-testid="api-documentation-page" >
- +
+ + + + +
+ + + + + diff --git a/src/views/Applications/ApplicationDetail.vue b/src/views/Applications/ApplicationDetail.vue index 2af40738..cd3b1d22 100644 --- a/src/views/Applications/ApplicationDetail.vue +++ b/src/views/Applications/ApplicationDetail.vue @@ -25,7 +25,7 @@ :is-rounded="false" :to="{ name: 'update-application' }" > - {{ helpText.edit }} + {{ helpText.application.edit }} @@ -35,7 +35,7 @@ class="flex-1" >

- {{ helpText.description }} + {{ helpText.application.description }}

{{ application.description }} @@ -46,18 +46,45 @@ v-if="application.redirect_uri" class="color-text_colors-secondary" > - {{ helpText.redirectUri(application.redirect_uri) }} + {{ helpText.application.redirectUri(application.redirect_uri) }}

- {{ helpText.referenceId(application.reference_id) }} + {{ helpText.application.referenceId(application.reference_id) }}

+
+ +

+ {{ analyticsCardTitle }} +

+ +
+ +
+
$route.params.application_id as string) const breadcrumbs = computed(() => ([{ key: 'my-apps', to: { name: 'my-apps' }, - text: helpText.breadcrumbMyApps + text: helpText.application.breadcrumbMyApps }])) const { portalApiV2 } = usePortalApi() - const appStore = useAppStore() - const { isDcr } = storeToRefs(appStore) + const { isDcr, allowedTimePeriod } = storeToRefs(appStore) + const vitalsLoading = ref(false) + const fixedTimeframe = allowedTimePeriod.value === PortalTimeframeKeys.NINETY_DAYS + ? ref(TimePeriods.get(TimeframeKeys.THIRTY_DAY)) + : ref(TimePeriods.get(TimeframeKeys.ONE_DAY)) const { state: currentState, send } = useMachine(createMachine({ predictableActionArguments: true, @@ -122,6 +159,10 @@ export default defineComponent({ } })) + const analyticsCardTitle = allowedTimePeriod.value === PortalTimeframeKeys.NINETY_DAYS + ? `${helpText.analytics.summary30Days} ${helpText.analytics.summary}` + : `${helpText.analytics.summary24Hours} ${helpText.analytics.summary}` + const fetchApplication = () => { send('FETCH') @@ -141,13 +182,16 @@ export default defineComponent({ }) return { + analyticsCardTitle, currentState, errorMessage, application, helpText, id, breadcrumbs, - isDcr + isDcr, + fixedTimeframe, + vitalsLoading } } }) diff --git a/src/views/Login.vue b/src/views/Login.vue index 70f46738..4f057a21 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -201,4 +201,17 @@ export default defineComponent({ #sign-up-encouragement-message .kong-icon { vertical-align: middle; } + +#kong-auth-login-wrapper { + [data-testid="kong-auth-login-sso"] { + color: var(--button_colors-primary-text) !important; + background-color: var(--button_colors-primary-fill) !important; + + svg { + path { + fill: var(--button_colors-primary-text) !important;; + } + } + } +} diff --git a/src/views/MyApps.vue b/src/views/MyApps.vue index e737bfc0..b1fdbd6b 100644 --- a/src/views/MyApps.vue +++ b/src/views/MyApps.vue @@ -31,6 +31,26 @@ +
+ +

+ {{ analyticsCardTitle(timeframe) }} +

+ + + +
+