diff --git a/.commitlintrc b/.commitlintrc new file mode 100644 index 00000000..d3d7f0cd --- /dev/null +++ b/.commitlintrc @@ -0,0 +1 @@ +{ "extends": ["@commitlint/config-conventional"] } diff --git a/.github/workflows/pr-agent.yaml b/.github/workflows/pr-agent.yaml deleted file mode 100644 index 49c7e50d..00000000 --- a/.github/workflows/pr-agent.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: CodiumAI PR Agent -on: - pull_request: - issue_comment: -jobs: - pr_agent: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - contents: write - steps: - - name: Exec PR Agent - id: pragent - uses: Codium-ai/pr-agent@main - env: - OPENAI_KEY: ${{ secrets.OPENAI_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests-ts.yaml b/.github/workflows/tests-ts.yaml index 6d0b7c84..0e09ccc3 100644 --- a/.github/workflows/tests-ts.yaml +++ b/.github/workflows/tests-ts.yaml @@ -30,6 +30,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Setup Node uses: actions/setup-node@v4 with: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..277d7981 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "proto/gateway"] + path = proto/gateway + url = https://github.com/basemind-ai/gateway-proto diff --git a/.idea/.name b/.idea/.name index bbd5466e..f5de4fc5 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -BaseMind.AI +Monorepo diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 5f5fce5a..0c1ab822 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,8 +1,52 @@ + + + + + + + + + + + + diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 80e519c3..0f7bc519 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,6 +1,5 @@ diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 89dc5c58..916c5fdd 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -43,7 +43,7 @@ postgresql true org.postgresql.Driver - jdbc:postgresql://localhost:5432/postgres + jdbc:postgresql://localhost:5432/basemind + diff --git a/.idea/modules.xml b/.idea/modules.xml index 66037823..ed248a10 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -1,8 +1,11 @@ - + - + - \ No newline at end of file + diff --git a/.idea/monorepo.iml b/.idea/monorepo.iml new file mode 100644 index 00000000..e83c15f7 --- /dev/null +++ b/.idea/monorepo.iml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index ab603aca..29206141 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aaa83294..bff7c461 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,22 +5,18 @@ repos: hooks: - id: commitlint stages: [commit-msg] - additional_dependencies: - - '@commitlint/cli' - - '@commitlint/config-conventional' + additional_dependencies: ['@commitlint/config-conventional'] - repo: https://github.com/pre-commit/pre-commit-hooks rev: 'v4.5.0' hooks: - id: trailing-whitespace - id: end-of-file-fixer - exclude: '.idea/modules' - id: check-yaml - id: check-added-large-files - repo: https://github.com/koalaman/shellcheck-precommit rev: v0.9.0 hooks: - id: shellcheck - exclude: 'gradle*' - repo: https://github.com/hadolint/hadolint rev: v2.12.1-beta hooks: @@ -29,14 +25,14 @@ repos: rev: 'v3.1.0' hooks: - id: prettier - exclude: 'go.mod|gen/ts|.idea/modules' + exclude: 'go.mod|gen/ts' - repo: https://github.com/pre-commit/mirrors-eslint - rev: 'v8.54.0' + rev: 'v8.55.0' hooks: - id: eslint files: \.tsx?$ types: [file] - args: [--fix] + args: [--fix, --no-ignore] exclude: 'gen/ts' - repo: https://github.com/golangci/golangci-lint rev: 'v1.55.2' diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..2ac0581a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "jsonRecursiveSort": true, + "plugins": ["@prettier/plugin-xml", "prettier-plugin-sort-json"], + "quoteProps": "consistent", + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "all", + "useTabs": true +} diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index d3a22707..00000000 --- a/.prettierrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - jsonRecursiveSort: true, - plugins: ['@prettier/plugin-xml', 'prettier-plugin-sort-json'], - quoteProps: 'consistent', - semi: true, - singleQuote: true, - tabWidth: 4, - trailingComma: 'all', - useTabs: true, -}; diff --git a/README.md b/README.md index 1d343621..62a9cfd3 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,6 @@ root # repository root, holding all tooling configuration │ ├─── openai # openai-connector protobuf schema │ ├─── cohere # cohere-connector protobuf schema │ └─── ptesting # api-gateway prompt testing protobuf schema -├─── sdks # client libraries that connect to our API gateway -│ └─── android # android apps -│ ├─── test-app # test application -│ └─── sdk # android sdk ├─── services # microservices │ ├─── api-gateway # api-gateway │ ├─── dashboard-backend # backend for the frontend web-app @@ -47,7 +43,6 @@ root # repository root, holding all tooling configuration - Go >= 1.21 - Docker >= 24.0 - Python >= 3.11 - - Java >= 17.0 2. Execute the setup task with: @@ -104,11 +99,6 @@ Configuration files that should not be committed into git are stored under the ` You will need to receive them from another developer and they must be communicated securely using a service such as [yopass](https://yopass.se/). -You will need to add the following files: - -- `.env.frontend` - this is an ENV file for the frontend application. -- `serviceAccountKey.json` - this is a GCP / firebase configuration file for backend applications. - ### Proto Files We use gRPC and protobuf files. The proto files are located under the `proto` folder and the generated code is stored diff --git a/Taskfile.yaml b/Taskfile.yaml index 43a8405b..4ee2e780 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -1,97 +1,32 @@ version: '3' tasks: - # project dependencies - install-pnpm: - cmds: - - | - if command -v brew &> /dev/null; then - brew install pnpm - else - npm install -g pnpm - fi - status: - - command -v pnpm &> /dev/null || exit 1 - install-pre-commit: - cmds: - - | - if command -v brew &> /dev/null; then - brew install pre-commit - else - pip install pre-commit - fi - status: - - command -v pre-commit &> /dev/null || exit 1 - install-sqlc: - cmds: - - | - if command -v brew &> /dev/null; then - brew install sqlc - else - go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest - fi - status: - - command -v sqlc &> /dev/null || exit 1 - install-atlas: - cmds: - - | - if command -v brew &> /dev/null; then - brew install ariga/tap/atlas - else - curl -sSf https://atlasgo.sh | sh - fi - status: - - command -v atlas &> /dev/null || exit 1 - install-buf: - cmds: - - | - if command -v brew &> /dev/null; then - brew install bufbuild/buf/buf - else - npm install -g @bufbuild/buf - fi - status: - - command -v buf &> /dev/null || exit 1 - install-terraform: - cmds: - - | - if command -v brew &> /dev/null; then - brew tap hashicorp/tap - brew install hashicorp/tap/terraform - brew install tflint - else - curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - - sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" - sudo apt-get update && sudo apt-get install terraform - curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash - fi - status: - - command -v terraform &> /dev/null || exit 1 + # project management update-brew: cmds: - - | - if command -v brew &> /dev/null; then - brew update; - brew upgrade; - brew cleanup || true; - fi + - brew update && brew upgrade && brew cleanup || true setup: desc: Setup the project dependencies cmds: - task: update-brew - - task: install-pnpm - - task: install-pre-commit - - task: install-sqlc - - task: install-atlas - - task: install-buf - - task: install-terraform - - pre-commit install && pre-commit install --hook-type commit-msg + - git submodule update --init --recursive || true + - git submodule update --recursive --remote + - command -v pnpm &> /dev/null || brew install pnpm + - command -v pre-commit &> /dev/null || brew install pre-commit + - command -v sqlc &> /dev/null || brew install sqlc + - command -v atlas &> /dev/null || brew install ariga/tap/atlas + - command -v buf &> /dev/null || brew install bufbuild/buf/buf + - command -v terraform &> /dev/null || brew tap hashicorp/tap && brew install hashicorp/tap/terraform + - command -v tflint &> /dev/null || brew install tflint + - command -v gcloud &> /dev/null || curl https://sdk.cloud.google.com | bash - pnpm install -r - go mod download + # - pre-commit install && pre-commit install --hook-type commit-msg && pre-commit install-hooks update: desc: Update the project dependencies cmds: - task: update-brew + - git submodule update --recursive --remote - pnpm add -g pnpm && pnpm update -r --latest - go mod tidy && go get -u ./... &> /dev/null - pre-commit autoupdate diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 422b1944..00000000 --- a/commitlint.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { extends: ['@commitlint/config-conventional'] }; diff --git a/frontend/package.json b/frontend/package.json index 59329709..42dcb57e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,27 +20,24 @@ "firebase": "^10.7.0", "firebaseui": "^6.1.0", "next": "14.0.3", - "next-intl": "^3.2.0", - "next-translate-plugin": "^2.6.2", + "next-intl": "^3.2.1", "react": "18.2.0", "react-bootstrap-icons": "^1.10.3", "react-dom": "18.2.0", - "react-intl": "^6.5.5", "react-syntax-highlighter": "^15.5.0", "react-tailwindcss-datepicker": "^1.6.6", "swr": "^2.2.4", - "unique-names-generator": "^4.7.1", "validator": "^13.11.0", "zustand": "^4.4.7" }, "devDependencies": { "@testing-library/dom": "^9.3.3", - "@testing-library/jest-dom": "^6.1.4", + "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "@types/react-syntax-highlighter": "^15.5.10", "@types/validator": "^13.11.7", "@vitejs/plugin-react": "^4.2.0", - "daisyui": "^4.4.14", + "daisyui": "^4.4.18", "jsdom": "^23.0.1", "mock-socket": "^9.3.1", "vite-plugin-magical-svg": "^1.0.3" diff --git a/frontend/public/images/logo-vector.svg b/frontend/public/images/logo-vector.svg new file mode 100644 index 00000000..e4c56037 --- /dev/null +++ b/frontend/public/images/logo-vector.svg @@ -0,0 +1,12 @@ + + + diff --git a/frontend/public/images/logo.svg b/frontend/public/images/logo.svg new file mode 100644 index 00000000..95741ff9 --- /dev/null +++ b/frontend/public/images/logo.svg @@ -0,0 +1,12 @@ + + + diff --git a/frontend/public/images/placholder-avatar.svg b/frontend/public/images/placholder-avatar.svg new file mode 100644 index 00000000..19ef4021 --- /dev/null +++ b/frontend/public/images/placholder-avatar.svg @@ -0,0 +1,16 @@ + + + + diff --git a/frontend/public/messages/en.json b/frontend/public/messages/en.json index f7729d42..d9f9b32c 100644 --- a/frontend/public/messages/en.json +++ b/frontend/public/messages/en.json @@ -51,6 +51,7 @@ "basemindName": "BaseMind", "cancel": "Cancel", "continue": "Continue", + "copied": "Copied", "decline": "Decline", "logout": "Logout", "open": "Open", @@ -77,6 +78,7 @@ "cohere": "Cohere", "configName": "Config Name", "continueButtonText": "Continue", + "createConfig": "Create Configuration", "createPromptConfigTitle": "New Model Configuration", "deleteMessage": "Remove Message", "duration": "Duration", @@ -92,7 +94,7 @@ "modelSelection": "Model", "modelType": "Model", "modelVendor": "Provider", - "newMessage": "New Message", + "newMessage": "+ New Message", "noVariablesHeadline": "The template has no variables - add as necessary or click on run test", "openai": "OpenAI", "openaiParametersFrequencyPenaltyLabel": "Frequency Penalty", @@ -116,6 +118,7 @@ "requestTokensCost": "Request Tokens Cost", "responseTokens": "Response Tokens", "responseTokensCost": "Response Tokens Cost", + "runTest": "Run Test", "runningTestError": "There was an error running test, try again later or contact support", "saveButtonText": "Save", "saveMessage": "Add Message", @@ -320,6 +323,7 @@ "createProviderKey": "Create Provider Key", "createdAt": "Created at", "deleteProviderKeyWarning": "Are you sure you want to delete this provider key?", + "headline": "Bring Your Own Key", "keyIsEncryptedMessage": "The API key is encrypted before being stored in our database.", "keyValue": "API Key", "modelVendor": "AI Provider", diff --git a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx index 9290a0e4..e691d5fc 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx @@ -187,11 +187,8 @@ describe('PromptConfigCreateWizard Page tests', () => { ).toBeInTheDocument(); }); - const saveButton = screen.getByTestId( - 'config-create-wizard-save-button', - ); - expect(saveButton).toBeInTheDocument(); - expect(saveButton).toBeDisabled(); + expect(continueButton).toBeInTheDocument(); + expect(continueButton).toBeDisabled(); }); it('allows the user to continue to the third stage if messages are not empty', async () => { @@ -231,52 +228,6 @@ describe('PromptConfigCreateWizard Page tests', () => { }); }); - it('allows the user to save the config if messages are not empty and replaces the route', async () => { - const promptConfig = OpenAIPromptConfigFactory.buildSync(); - mockFetch.mockResolvedValue({ - json: () => Promise.resolve(promptConfig), - ok: true, - }); - - const store = getStore(); - act(() => { - store.setConfigName(promptConfig.name); - store.setMessages(promptConfig.providerPromptMessages); - store.setParameters(promptConfig.modelParameters); - store.setModelType(promptConfig.modelType); - store.setModelVendor(promptConfig.modelVendor); - }); - render( - , - ); - - const continueButton = screen.getByTestId( - 'config-create-wizard-continue-button', - ); - expect(continueButton).toBeInTheDocument(); - fireEvent.click(continueButton); - - await waitFor(() => { - expect( - screen.getByTestId('parameters-and-prompt-form-container'), - ).toBeInTheDocument(); - }); - - const saveButton = screen.getByTestId( - 'config-create-wizard-save-button', - ); - expect(saveButton).toBeInTheDocument(); - expect(saveButton).not.toBeDisabled(); - - fireEvent.click(saveButton); - - await waitFor(() => { - expect(mockFetch).toHaveBeenCalledTimes(1); - }); - - expect(routerReplaceMock).toHaveBeenCalledOnce(); - }); - it('allows the user to save the config when on the third stage and replaces the route', async () => { const promptConfig = OpenAIPromptConfigFactory.buildSync(); mockFetch.mockResolvedValue({ @@ -343,20 +294,16 @@ describe('PromptConfigCreateWizard Page tests', () => { store.setParameters(promptConfig.modelParameters); store.setModelType(promptConfig.modelType); store.setModelVendor(promptConfig.modelVendor); + store.setNextWizardStage(); + store.setNextWizardStage(); }); render( , ); - const continueButton = screen.getByTestId( - 'config-create-wizard-continue-button', - ); - expect(continueButton).toBeInTheDocument(); - fireEvent.click(continueButton); - await waitFor(() => { expect( - screen.getByTestId('parameters-and-prompt-form-container'), + screen.getByTestId('prompt-config-testing-form'), ).toBeInTheDocument(); }); @@ -473,7 +420,7 @@ describe('PromptConfigCreateWizard Page tests', () => { expect(continueButton.length).toBeFalsy(); }); - it('allows the user to cancel at all stages', async () => { + it('allows the user to cancel only on the first stage', async () => { const promptConfig = OpenAIPromptConfigFactory.buildSync(); const store = getStore(); @@ -495,15 +442,11 @@ describe('PromptConfigCreateWizard Page tests', () => { screen.getByTestId('prompt-config-testing-form'), ).toBeInTheDocument(); }); - const cancelButton = screen.getByTestId( + + let cancelButton = screen.queryByTestId( 'config-create-wizard-cancel-button', ); - expect(cancelButton).toBeInTheDocument(); - expect(cancelButton).not.toBeDisabled(); - - fireEvent.click(cancelButton); - - expect(routerPushMock).toHaveBeenCalledOnce(); + expect(cancelButton).toBeNull(); act(() => { store.setPrevWizardStage(); @@ -515,11 +458,10 @@ describe('PromptConfigCreateWizard Page tests', () => { ).toBeInTheDocument(); }); - expect(cancelButton).not.toBeDisabled(); - - fireEvent.click(cancelButton); - - expect(routerPushMock).toHaveBeenCalledTimes(2); + cancelButton = screen.queryByTestId( + 'config-create-wizard-cancel-button', + ); + expect(cancelButton).toBeNull(); act(() => { store.setPrevWizardStage(); @@ -530,12 +472,12 @@ describe('PromptConfigCreateWizard Page tests', () => { screen.getByTestId('base-form-container'), ).toBeInTheDocument(); }); - + cancelButton = screen.getByTestId('config-create-wizard-cancel-button'); expect(cancelButton).not.toBeDisabled(); fireEvent.click(cancelButton); - expect(routerPushMock).toHaveBeenCalledTimes(3); + expect(routerPushMock).toHaveBeenCalledTimes(1); }); it('shows the provider key create modal when no provider key exists for the given vendor', async () => { diff --git a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.tsx index d7eeb6d6..7879e394 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.tsx @@ -14,6 +14,7 @@ import { PromptConfigParametersAndPromptForm } from '@/components/projects/[proj import { PromptConfigTestingForm } from '@/components/projects/[projectId]/applications/[applicationId]/config-create-wizard/prompt-config-testing-form'; import { ProviderKeyCreateModal } from '@/components/projects/[projectId]/provider-key-create-modal'; import { Navigation } from '@/constants'; +import { useAuthenticatedUser } from '@/hooks/use-authenticated-user'; import { useHandleError } from '@/hooks/use-handle-error'; import { useSwrProviderKeys } from '@/hooks/use-swr-provider-keys'; import { @@ -29,6 +30,8 @@ import { import { ProviderKey } from '@/types'; import { setRouteParams } from '@/utils/navigation'; +const stepColor = 'step-secondary'; +const stepper = Object.values(WizardStage).filter((v) => typeof v === 'number'); export default function PromptConfigCreateWizard({ params: { applicationId, projectId }, }: { @@ -41,6 +44,7 @@ export default function PromptConfigCreateWizard({ const [nameIsValid, setNameIsValid] = useState(false); + const user = useAuthenticatedUser(); const project = useProject(projectId); const application = useApplication(projectId, applicationId); @@ -195,81 +199,99 @@ export default function PromptConfigCreateWizard({ }; return ( -
+
{isLoading ? (
) : ( <> - -
- {wizardStageComponentMap[store.wizardStage]} - {store.wizardStage < 2 && ( -
- )} -
- -
- {store.wizardStage > 0 && ( - - )} - {store.wizardStage >= 1 && ( - - )} - {store.wizardStage < 2 && ( - - )} + }`} + /> + ))} + +
+
+ {wizardStageComponentMap[store.wizardStage]} +
+
+
+ {store.wizardStage === 0 && ( + + )} + {store.wizardStage > 0 && ( + + )} + {store.wizardStage === 2 && ( + + )} + {store.wizardStage < 2 && ( + + )} +
diff --git a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/configs/[promptConfigId]/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/configs/[promptConfigId]/page.tsx index 131e878b..5b8ab352 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/configs/[promptConfigId]/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/configs/[promptConfigId]/page.tsx @@ -8,6 +8,7 @@ import useSWR from 'swr'; import { handleRetrievePromptConfigs } from '@/api'; import { Navbar } from '@/components/navbar'; import { PromptConfigAnalyticsPage } from '@/components/projects/[projectId]/applications/[applicationId]/configs/[configId]/prompt-config-analytics-page'; +import { PromptConfigCodeSnippet } from '@/components/projects/[projectId]/applications/[applicationId]/configs/[configId]/prompt-config-code-snippet'; import { PromptConfigDeletion } from '@/components/projects/[projectId]/applications/[applicationId]/configs/[configId]/prompt-config-deletion'; import { PromptConfigGeneralInfo } from '@/components/projects/[projectId]/applications/[applicationId]/configs/[configId]/prompt-config-general-info'; import { PromptConfigGeneralSettings } from '@/components/projects/[projectId]/applications/[applicationId]/configs/[configId]/prompt-config-general-settings'; @@ -33,7 +34,7 @@ export default function PromptConfiguration({ promptConfigId: string; }; }) { - useAuthenticatedUser(); + const user = useAuthenticatedUser(); useProjectBootstrap(false); const t = useTranslations('promptConfig'); @@ -100,7 +101,9 @@ export default function PromptConfiguration({ applicationId={applicationId} promptConfig={promptConfig} /> -
+
+ +
)), @@ -131,26 +134,27 @@ export default function PromptConfiguration({ const TabComponent = tabComponents[selectedTab]; return ( -
- -
+
+ tabs={tabs} selectedTab={selectedTab} onTabChange={setSelectedTab} trailingLine={true} /> -
-
+
+
-
+ ); } diff --git a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/page.tsx index 24f234a2..d30de512 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/page.tsx @@ -21,7 +21,7 @@ export default function Application({ }: { params: { applicationId: string; projectId: string }; }) { - useAuthenticatedUser(); + const user = useAuthenticatedUser(); useProjectBootstrap(false); const t = useTranslations('application'); @@ -94,16 +94,19 @@ export default function Application({ data-testid="application-page" className="flex flex-col min-h-screen w-full bg-base-100" > - -
+
+ tabs={tabs} selectedTab={selectedTab} onTabChange={setSelectedTab} trailingLine={true} /> -
-
+
diff --git a/frontend/src/app/[locale]/projects/[projectId]/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/page.tsx index f746edec..29d96919 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/page.tsx @@ -23,7 +23,7 @@ export default function ProjectOverview({ }: { params: { projectId: string }; }) { - useAuthenticatedUser(); + const user = useAuthenticatedUser(); useProjectBootstrap(); const t = useTranslations('projectOverview'); @@ -100,16 +100,15 @@ export default function ProjectOverview({ className="flex flex-col min-h-screen w-full bg-base-100" data-testid="project-page" > - -
+
+ tabs={tabs} selectedTab={selectedTab} onTabChange={setSelectedTab} trailingLine={true} /> -
-
+
diff --git a/frontend/src/app/[locale]/projects/page.tsx b/frontend/src/app/[locale]/projects/page.tsx index 6553b756..467c6c3f 100644 --- a/frontend/src/app/[locale]/projects/page.tsx +++ b/frontend/src/app/[locale]/projects/page.tsx @@ -9,7 +9,7 @@ export default function Projects() { return (
); diff --git a/frontend/src/app/[locale]/settings/page.tsx b/frontend/src/app/[locale]/settings/page.tsx index 7a404da5..e794be3f 100644 --- a/frontend/src/app/[locale]/settings/page.tsx +++ b/frontend/src/app/[locale]/settings/page.tsx @@ -7,7 +7,7 @@ import { UserDetails } from '@/components/settings/user-details'; import { useAuthenticatedUser } from '@/hooks/use-authenticated-user'; export default function UserSettings() { - const user = useAuthenticatedUser()!; + const user = useAuthenticatedUser(); const t = useTranslations('userSettings'); return ( @@ -15,11 +15,14 @@ export default function UserSettings() { data-testid="user-settings-page" className="flex flex-col min-h-screen w-full bg-base-100" > - -
-
- -
+
+ +
+ +
diff --git a/frontend/src/app/[locale]/support/page.tsx b/frontend/src/app/[locale]/support/page.tsx index 0acf90bc..0d5a4548 100644 --- a/frontend/src/app/[locale]/support/page.tsx +++ b/frontend/src/app/[locale]/support/page.tsx @@ -15,12 +15,13 @@ export default function Support() { data-testid="support-page" className="flex flex-col min-h-screen w-full bg-base-100" > - - -
-
- -
+
+ +
+
diff --git a/frontend/src/components/avatar-dropdown.spec.tsx b/frontend/src/components/avatar-dropdown.spec.tsx new file mode 100644 index 00000000..8810cffd --- /dev/null +++ b/frontend/src/components/avatar-dropdown.spec.tsx @@ -0,0 +1,134 @@ +import { ProjectFactory } from 'tests/factories'; +import { fireEvent, render, screen } from 'tests/test-utils'; +import { expect } from 'vitest'; + +import { AvatarDropdown } from '@/components/avatar-dropdown'; +import { Navigation } from '@/constants'; + +describe('AvatarDropdown tests', () => { + const mockHandleSetProject = vi.fn(); + const projects = ProjectFactory.batchSync(3); + const userPhotoURL = '/images/placholder-avatar.svg'; + + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('renders AvatarDropdown with projects', () => { + render( + , + ); + + const avatar = screen.getByTestId('avatar-image'); + expect(avatar).toBeInTheDocument(); + + projects.forEach((project) => { + const projectLink = screen.getByTestId( + `project-select-link-${project.id}`, + ); + expect(projectLink).toBeInTheDocument(); + }); + + const settingsLink = screen.getByTestId('setting-link'); + expect(settingsLink).toBeInTheDocument(); + + const supportLink = screen.getByTestId('support-link'); + expect(supportLink).toBeInTheDocument(); + }); + + it('handles project selection', () => { + render( + , + ); + + const projectLink = screen.getByTestId( + `project-select-link-${projects[0].id}`, + ); + fireEvent.click(projectLink); + expect(mockHandleSetProject).toHaveBeenCalledWith(projects[0].id); + }); + + it('navigates to settings', () => { + render( + , + ); + + const settingsLink = screen.getByTestId('setting-link'); + expect(settingsLink).toHaveAttribute('href', Navigation.Settings); + }); + + it('navigates to support', () => { + render( + , + ); + + const supportLink = screen.getByTestId('support-link'); + expect(supportLink).toHaveAttribute('href', Navigation.Support); + }); + + it('navigates to create new project', () => { + const projects = ProjectFactory.batchSync(3); + render( + , + ); + + const createNewProjectLink = screen.getByTestId( + 'create-new-project-link', + ); + expect(createNewProjectLink).toBeInTheDocument(); + expect(createNewProjectLink).toHaveAttribute( + 'href', + Navigation.CreateProject, + ); + }); + it('renders logout button and triggers logout on click', () => { + const projects = ProjectFactory.batchSync(3); + render( + , + ); + + const logoutButton = screen.getByTestId('dashboard-logout-btn'); // Replace 'logout-button' with the actual test ID of your LogoutButton if different + expect(logoutButton).toBeInTheDocument(); + }); + + it('displays correct project names', () => { + render( + , + ); + + projects.forEach((project) => { + const projectLink = screen.getByTestId( + `project-select-link-${project.id}`, + ); + expect(projectLink).toHaveTextContent(project.name); + }); + }); +}); diff --git a/frontend/src/components/avatar-dropdown.tsx b/frontend/src/components/avatar-dropdown.tsx new file mode 100644 index 00000000..14b892b5 --- /dev/null +++ b/frontend/src/components/avatar-dropdown.tsx @@ -0,0 +1,84 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { useTranslations } from 'next-intl'; + +import { LogoutButton } from '@/components/settings/logout-button'; +import { Navigation } from '@/constants'; +import { Project } from '@/types'; +import { setRouteParams } from '@/utils/navigation'; + +export function AvatarDropdown({ + userPhotoURL, + projects, + handleSetProject, +}: { + handleSetProject: (projectId: string) => void; + projects: Project[]; + userPhotoURL: string; +}) { + const t = useTranslations('navbar'); + + return ( +
+
+
+ Logo +
+
+
    +
  • + + {t('settings')} + +
  • +
  • + + {t('support')} + +
  • +
    + {projects.map((nonActiveproject) => ( +
  • + { + handleSetProject(nonActiveproject.id); + }} + data-testid={`project-select-link-${nonActiveproject.id}`} + > + {nonActiveproject.name} + +
  • + ))} +
  • + + {t('createNewProject')} + +
  • +
  • + +
  • +
    +
+
+ ); +} diff --git a/frontend/src/components/code-snippet.spec.tsx b/frontend/src/components/code-snippet.spec.tsx new file mode 100644 index 00000000..bf39d118 --- /dev/null +++ b/frontend/src/components/code-snippet.spec.tsx @@ -0,0 +1,124 @@ +import { render, renderHook, screen, waitFor } from 'tests/test-utils'; +import { expect } from 'vitest'; + +import { CodeSnippet } from '@/components/code-snippet'; +import { useToasts } from '@/stores/toast-store'; + +const writeTextMock = vi.fn(); + +// @ts-expect-error +navigator.clipboard = { writeText: writeTextMock }; + +describe('CodeSnippet', () => { + it('should render code snippet with specified language and style', () => { + const codeText = 'const x = 5;'; + const language = 'javascript'; + + render( + , + ); + + const codeSnippet = screen.getByTestId('code-snippet-javascript'); + expect(codeSnippet).toBeInTheDocument(); + }); + + it('should set data-testid attribute with language value', () => { + const codeText = 'const x = 5;'; + const language = 'javascript'; + + render( + , + ); + + const codeSnippet = screen.getByTestId('code-snippet-javascript'); + expect(codeSnippet).toBeInTheDocument(); + }); + + it('should display copy button when allowCopy is true', () => { + const codeText = 'const x = 5;'; + const language = 'javascript'; + const allowCopy = true; + + render( + , + ); + + const copyButton = screen.getByTestId('code-snippet-code-copy-button'); + expect(copyButton).toBeInTheDocument(); + }); + + it('should copy code to clipboard when copy button is clicked', () => { + const codeText = 'const x = 5;'; + const language = 'javascript'; + const allowCopy = true; + + render( + , + ); + + const copyButton = screen.getByTestId('code-snippet-code-copy-button'); + copyButton.click(); + + expect(writeTextMock).toHaveBeenCalledWith(codeText); + }); + + it('should show success message when code is copied', async () => { + const codeText = 'const x = 5;'; + const language = 'javascript'; + const allowCopy = true; + + render( + , + ); + + const { + result: { current }, + } = renderHook(useToasts); + + const copyButton = screen.getByTestId('code-snippet-code-copy-button'); + copyButton.click(); + + await waitFor(() => { + expect(current.length).toBe(1); + }); + }); + + it('should not display copy button when allowCopy is false', () => { + const codeText = 'const x = 5;'; + const language = 'javascript'; + const allowCopy = false; + + render( + , + ); + + const copyButton = screen.queryByTestId( + 'code-snippet-code-copy-button', + ); + expect(copyButton).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/code-snippet.tsx b/frontend/src/components/code-snippet.tsx new file mode 100644 index 00000000..0dc91b44 --- /dev/null +++ b/frontend/src/components/code-snippet.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { Front } from 'react-bootstrap-icons'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { darcula } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +import { useShowSuccess } from '@/stores/toast-store'; +import { copyToClipboard } from '@/utils/helpers'; + +export function CodeSnippet({ + codeText, + language, + allowCopy, +}: { + allowCopy: boolean; + codeText: string; + language: string; +}) { + const showSuccess = useShowSuccess(); + const t = useTranslations('common'); + + return ( +
+ } + lineNumberStyle={{ color: '#ccc' }} + className="text-xs rounded-3xl bg-base-300" + > + {codeText} + + {allowCopy && ( + + )} +
+ ); +} diff --git a/frontend/src/components/date-picker.tsx b/frontend/src/components/date-picker.tsx index 924e8a0b..be62cb45 100644 --- a/frontend/src/components/date-picker.tsx +++ b/frontend/src/components/date-picker.tsx @@ -21,7 +21,7 @@ export function DatePicker({
{t('basemindName')} diff --git a/frontend/src/components/modal.tsx b/frontend/src/components/modal.tsx index 2091ac58..920deb90 100644 --- a/frontend/src/components/modal.tsx +++ b/frontend/src/components/modal.tsx @@ -27,7 +27,7 @@ export function Modal({ return ( -
+
{children}
diff --git a/frontend/src/components/navbar.spec.tsx b/frontend/src/components/navbar.spec.tsx index 639ffb65..49d185ba 100644 --- a/frontend/src/components/navbar.spec.tsx +++ b/frontend/src/components/navbar.spec.tsx @@ -111,7 +111,7 @@ describe('Navbar tests', () => { const projects = ProjectFactory.batchSync(3); result.current(projects); render(); - const dropdown = screen.getByTestId('selected-project'); + const dropdown = screen.getByTestId('avatar-image'); expect(dropdown).toBeInTheDocument(); fireEvent.click(dropdown); @@ -138,7 +138,7 @@ describe('Navbar tests', () => { useSetProjectsHook.current(projects); setSelectedProjectHook.current(projects[0].id); render(); - const dropdown = screen.getByTestId('selected-project'); + const dropdown = screen.getByTestId('avatar-image'); fireEvent.click(dropdown); fireEvent.click( screen.getByTestId(`project-select-link-${projects[1].id}`), @@ -151,7 +151,7 @@ describe('Navbar tests', () => { it('picking create new project should navigate to create project screen', async () => { const projects = ProjectFactory.batchSync(3); render(); - const dropdown = screen.getByTestId('selected-project'); + const dropdown = screen.getByTestId('avatar-image'); fireEvent.click(dropdown); const createNewProjectLink = screen.getByTestId( 'create-new-project-link', diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index e230f0ad..78872e4f 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -1,10 +1,9 @@ 'use client'; -import Image from 'next/image'; import Link from 'next/link'; -import { useTranslations } from 'next-intl'; import { ArrowLeft } from 'react-bootstrap-icons'; -import { LogoutButton } from '@/components/settings/logout-button'; +import { AvatarDropdown } from '@/components/avatar-dropdown'; +import { Logo } from '@/components/logo'; import { Navigation } from '@/constants'; import { useProjects, useSetSelectedProject } from '@/stores/api-store'; import { Application, Project, PromptConfig } from '@/types'; @@ -15,46 +14,40 @@ export function Navbar({ application, config, headline, + userPhotoURL, }: { application?: Application; config?: PromptConfig; headline?: string; project?: Project; + userPhotoURL?: string | null; }) { - const t = useTranslations('navbar'); const projects = useProjects(); const setSelectedProject = useSetSelectedProject(); - const backLink = projects[0]?.id - ? setRouteParams(Navigation.ProjectDetail, { - projectId: projects[0]?.id, - }) - : Navigation.CreateProject; + return ( -
-
- Logo +
+
+ {headline && ( <> - + -

+

{headline}

)} {project && ( -
+
  • )}
- -
-
    -
  • - - {t('settings')} - -
  • -
  • - - {t('support')} - -
  • - {project && ( -
  • -
    - - {project.name} - -
      - {projects.map((nonActiveproject) => ( -
    • - { - setSelectedProject( - nonActiveproject.id, - ); - }} - className={ - nonActiveproject.id === - project.id - ? 'selected' - : '' - } - data-testid={`project-select-link-${nonActiveproject.id}`} - > - {nonActiveproject.name} - -
    • - ))} -
    • - - {t('createNewProject')} - -
    • -
    -
    -
  • - )} - {!project && headline && } -
-
+
); } diff --git a/frontend/src/components/navrail/navrail-badge.tsx b/frontend/src/components/navrail/navrail-badge.tsx deleted file mode 100644 index 7958ef8c..00000000 --- a/frontend/src/components/navrail/navrail-badge.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export interface BadgeProps { - fillColor: string; - text: string; - textColor: string; -} - -export function NavrailBadge({ fillColor, textColor, text }: BadgeProps) { - return ( - - {text} - - ); -} diff --git a/frontend/src/components/navrail/navrail-footer.spec.tsx b/frontend/src/components/navrail/navrail-footer.spec.tsx deleted file mode 100644 index bc30c586..00000000 --- a/frontend/src/components/navrail/navrail-footer.spec.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { usePathname } from 'next/navigation'; -import { render, screen } from 'tests/test-utils'; -import { Mock } from 'vitest'; - -import { NavRailFooter } from '@/components/navrail/navrail-footer'; -import { Navigation } from '@/constants'; - -describe('NavRailFooter tests', () => { - (usePathname as Mock).mockReturnValue(`${Navigation.Settings}?4242`); - - it('should highlight the correct link based on pathname', () => { - render(); - - const settingsLink = screen.getByTestId('nav-rail-footer-settings'); - expect(settingsLink.className).toContain('text-primary'); - }); - - it('should not highlight links on different urls', () => { - render(); - - const billingLink = screen.getByTestId('nav-rail-footer-support'); - expect(billingLink.className).toContain('text-base-content'); - }); -}); diff --git a/frontend/src/components/navrail/navrail-footer.tsx b/frontend/src/components/navrail/navrail-footer.tsx deleted file mode 100644 index 99d5e463..00000000 --- a/frontend/src/components/navrail/navrail-footer.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { usePathname } from 'next/navigation'; -import { Gear, QuestionCircle } from 'react-bootstrap-icons'; - -import { Navigation } from '@/constants'; - -export function NavRailFooter() { - const [pathname] = usePathname().split('?'); - - const linkStyle = (linkPath: Navigation) => - linkPath === pathname ? 'text-primary' : 'text-base-content'; - - return ( - - ); -} diff --git a/frontend/src/components/navrail/navrail-link-menu.spec.tsx b/frontend/src/components/navrail/navrail-link-menu.spec.tsx deleted file mode 100644 index 53de78e4..00000000 --- a/frontend/src/components/navrail/navrail-link-menu.spec.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { render, screen } from 'tests/test-utils'; - -import { - NavrailLinkMenu, - NavrailLinkMenuProps, -} from '@/components/navrail/navrail-link-menu'; - -describe('NavrailLinkMenu tests', () => { - const props: NavrailLinkMenuProps = { - badge:

Badge

, - children:

Child

, - href: 'http://example.com/', - icon:

Icon

, - isCurrent: true, - text: 'Navigation', - }; - - it('renders LinkMenu correctly', () => { - render(); - - const text = screen.getByText(props.text!); - expect(text).toBeInTheDocument(); - - const icon = screen.getByText('Icon'); - expect(icon).toBeInTheDocument(); - - const badge = screen.getByText('Badge'); - expect(badge).toBeInTheDocument(); - - const child = screen.getByText('Child'); - expect(child).toBeInTheDocument(); - - const link = screen.getByTestId('link-menu-anchor'); - expect(link.href).toBe(props.href); - }); -}); diff --git a/frontend/src/components/navrail/navrail-link-menu.tsx b/frontend/src/components/navrail/navrail-link-menu.tsx deleted file mode 100644 index 27e58536..00000000 --- a/frontend/src/components/navrail/navrail-link-menu.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; -import Link from 'next/link'; - -export interface NavrailLinkMenuProps { - badge?: React.ReactNode; - children?: React.ReactNode; - href?: string; - icon?: React.ReactElement; - isCurrent?: boolean; - isDisabled?: boolean; - text?: string; -} - -export function NavrailLinkMenu({ - text, - icon, - badge, - isDisabled, - isCurrent, - href, - children, -}: NavrailLinkMenuProps) { - const Wrapper = ({ children }: { children: React.ReactNode }) => - isDisabled ? ( - <>{children} - ) : ( - - {children} - - ); - - return ( - <> - -
-
- {icon &&
{icon}
} - {text && {text}} -
- {badge && {badge}} -
-
- {children && ( -
- {children} -
- )} - - ); -} diff --git a/frontend/src/components/navrail/navrail-list.tsx b/frontend/src/components/navrail/navrail-list.tsx deleted file mode 100644 index d31a9a9c..00000000 --- a/frontend/src/components/navrail/navrail-list.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { usePathname } from 'next/navigation'; -import { useTranslations } from 'next-intl'; -import { useRef } from 'react'; -import { - Boxes, - HddStack, - HouseDoor, - Speedometer2, -} from 'react-bootstrap-icons'; - -import { NavrailBadge } from '@/components/navrail/navrail-badge'; -import { NavrailLinkMenu } from '@/components/navrail/navrail-link-menu'; -import { CreateApplication } from '@/components/projects/[projectId]/applications/create-application'; -import { useApplications, useSelectedProject } from '@/stores/api-store'; -import { contextNavigation, setApplicationId } from '@/utils/navigation'; - -const ICON_CLASSES = 'w-3.5 h-3.5'; - -function NewApplication({ projectId }: { projectId?: string }) { - const t = useTranslations('navrail'); - const dialogRef = useRef(null); - - if (!projectId) { - return null; - } - - return ( - <> - - -
- dialogRef.current?.close()} - /> -
- -
- - ); -} - -export function NavRailList() { - const t = useTranslations('navrail'); - const [pathname] = usePathname().split('?'); - // TODO: Remove this hook if current project can be ALWAYS derived from path - const currentProject = useSelectedProject(); - const navigation = contextNavigation(currentProject?.id); - const projectApplications = useApplications(currentProject?.id); - - return ( -
- } - isCurrent={navigation.ProjectDetail === pathname} - /> - } - > - {projectApplications?.map((application) => { - const applicationUrl = setApplicationId( - navigation.ApplicationDetail, - application.id, - ); - return ( - - ); - })} - - - } - isDisabled={true} - badge={ - - } - /> - } - isDisabled={true} - badge={ - - } - /> -
- ); -} diff --git a/frontend/src/components/navrail/navrail.spec.tsx b/frontend/src/components/navrail/navrail.spec.tsx deleted file mode 100644 index 14ce79e1..00000000 --- a/frontend/src/components/navrail/navrail.spec.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { fireEvent } from '@testing-library/react'; -import { usePathname } from 'next/navigation'; -import locales from 'public/messages/en.json'; -import { ApplicationFactory, ProjectFactory } from 'tests/factories'; -import { render, renderHook, screen } from 'tests/test-utils'; -import { expect, Mock } from 'vitest'; - -import { NavRail } from '@/components/navrail/navrail'; -import { Navigation } from '@/constants'; -import { - useSetProjectApplications, - useSetProjects, - useSetSelectedProject, -} from '@/stores/api-store'; - -const navRailTranslation = locales.navrail; - -describe('NavRail tests', () => { - (usePathname as Mock).mockReturnValue(Navigation.ProjectDetail); - - const showModal = vi.fn(); - const closeModal = vi.fn(); - - beforeAll(() => { - HTMLDialogElement.prototype.showModal = showModal; - HTMLDialogElement.prototype.close = closeModal; - }); - - it('should render Logo', () => { - render(); - expect(screen.getByTestId('logo-component')).toBeInTheDocument(); - }); - - it('should render NavRailList', () => { - const projects = ProjectFactory.batchSync(2); - const { - result: { current: setProjects }, - } = renderHook(useSetProjects); - setProjects(projects); - const { - result: { current: setCurrentProject }, - } = renderHook(useSetSelectedProject); - setCurrentProject(projects[0].id); - const { - result: { current: setProjectApplications }, - } = renderHook(useSetProjectApplications); - setProjectApplications(projects[0].id, ApplicationFactory.batchSync(2)); - - render(); - expect(screen.getByTestId('nav-rail-list')).toBeInTheDocument(); - }); - - it('should render NavRailFooter', () => { - render(); - expect(screen.getByTestId('nav-rail-footer')).toBeInTheDocument(); - }); - - it('uses translated text', () => { - render(); - const overviewItem = screen.getByText(navRailTranslation.overview); - expect(overviewItem).toBeInTheDocument(); - }); - - it('shows and hides create application dialog', async () => { - const projects = ProjectFactory.batchSync(2); - const { - result: { current: setProjects }, - } = renderHook(useSetProjects); - setProjects(projects); - const { - result: { current: setCurrentProject }, - } = renderHook(useSetSelectedProject); - setCurrentProject(projects[0].id); - - render(); - - const createAppButton = screen.getByTestId( - 'nav-rail-create-application-btn', - ); - expect(createAppButton).toBeInTheDocument(); - - fireEvent.click(createAppButton); - expect(showModal).toHaveBeenCalledOnce(); - - const cancelButton = screen.getByTestId( - 'create-application-cancel-btn', - ); - fireEvent.click(cancelButton); - - expect(closeModal).toHaveBeenCalledOnce(); - }); -}); diff --git a/frontend/src/components/navrail/navrail.tsx b/frontend/src/components/navrail/navrail.tsx deleted file mode 100644 index 16b8f524..00000000 --- a/frontend/src/components/navrail/navrail.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import { Logo } from '@/components/logo'; -import { NavRailFooter } from '@/components/navrail/navrail-footer'; -import { NavRailList } from '@/components/navrail/navrail-list'; - -export function NavRail() { - return ( -
-
- - -
- -
- ); -} diff --git a/frontend/src/components/projects/[projectId]/applications/[applicationId]/application-analytics-page.tsx b/frontend/src/components/projects/[projectId]/applications/[applicationId]/application-analytics-page.tsx index b0a7c594..a2309f7f 100644 --- a/frontend/src/components/projects/[projectId]/applications/[applicationId]/application-analytics-page.tsx +++ b/frontend/src/components/projects/[projectId]/applications/[applicationId]/application-analytics-page.tsx @@ -46,7 +46,7 @@ export function ApplicationAnalyticsPage({ return (
-

{t('status')}

+

{t('status')}