diff --git a/.github/workflows/frontend_ci_cd.yml b/.github/workflows/frontend_ci_cd.yml index e49c01c..e28fe43 100644 --- a/.github/workflows/frontend_ci_cd.yml +++ b/.github/workflows/frontend_ci_cd.yml @@ -12,9 +12,36 @@ on: - ui/** jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: 'ui/.nvmrc' + cache: 'npm' + cache-dependency-path: 'ui/package-lock.json' + + - name: Install dependencies + run: npm ci + working-directory: ./ui + + - name: Run Prettier format check + run: npm run format:check + working-directory: ./ui + + - name: Run ESLint + run: npm run lint + working-directory: ./ui + # Currently there are no tests to run before build build: + needs: lint runs-on: ubuntu-latest # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. diff --git a/.github/workflows/on-pull-request.yaml b/.github/workflows/on-pull-request.yaml index 8af7d49..80c2e81 100644 --- a/.github/workflows/on-pull-request.yaml +++ b/.github/workflows/on-pull-request.yaml @@ -49,6 +49,32 @@ jobs: run: poetry run test working-directory: ./backend + lint-frontend: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: 'ui/.nvmrc' + cache: 'npm' + cache-dependency-path: 'ui/package-lock.json' + + - name: Install dependencies + run: npm ci + working-directory: ./ui + + - name: Run Prettier format check + run: npm run format:check + working-directory: ./ui + + - name: Run ESLint + run: npm run lint + working-directory: ./ui + lint-helm: runs-on: ubuntu-latest steps: diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json index 64c0f28..6e13c48 100644 --- a/ui/.eslintrc.json +++ b/ui/.eslintrc.json @@ -23,6 +23,7 @@ }, "plugins": ["react-refresh"], "rules": { + "react/prop-types": "off", "react/jsx-no-target-blank": "off", "react-refresh/only-export-components": [ "warn", diff --git a/ui/public/icons/copy.svg b/ui/public/icons/copy.svg new file mode 100644 index 0000000..f2a68cd --- /dev/null +++ b/ui/public/icons/copy.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/ui/src/App.css b/ui/src/App.css index 8b7e19d..51c6984 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -1,40 +1,41 @@ :root { - --color-sherpa100: #D9F4F4; - --color-sherpa200: #B8E8E9; - --color-sherpa300: #86D6DA; - --color-sherpa400: #63CBCF; - --color-sherpa500: #009AA1; + --color-sherpa100: #d9f4f4; + --color-sherpa200: #b8e8e9; + --color-sherpa300: #86d6da; + --color-sherpa400: #63cbcf; + --color-sherpa500: #009aa1; --color-sherpa600: #087a82; - --color-sherpa700: #0C646D; - --color-sherpa800: #004F59; - --color-sherpa900: #0C3B41; - --color-white: #FFFFFF; + --color-sherpa700: #0c646d; + --color-sherpa800: #004f59; + --color-sherpa900: #0c3b41; + --color-white: #ffffff; --color-black: #000000; - --color-yellow: #F1B828; + --color-yellow: #f1b828; + --color-yellow-hover: #e0a71f; + --color-yellow-active: #c99319; + --color-red: #bf6366; + --color-red-hover: #a65558; + --color-red-active: #8d484b; + --color-white-transparent: rgba(255, 255, 255, 0.1); } .App { min-height: 100vh; display: flex; flex-direction: column; - /* Desktop background (default) */ - background-image: - linear-gradient(180deg, rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.00) 15.58%), - radial-gradient(68.05% 68.05% at 50.79% 4.4%, rgba(12, 59, 65, 0.30) 0%, #0C3B41 100%), + /* Desktop background (default) */ + background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 15.58%), + radial-gradient(68.05% 68.05% at 50.79% 4.4%, rgba(12, 59, 65, 0.3) 0%, #0c3b41 100%), url('/background-image-desktop.jpg'); background-position: center, center, top; - background-size: 100% 100%, 100% 100%, cover; + background-size: + 100% 100%, + 100% 100%, + cover; background-repeat: no-repeat; background-attachment: fixed; } -#infoPanel { - text-align: left; - font-style: italic; - font-weight: bold; - color: rgb(126, 52, 52); -} - /* Button Group Container */ .button-group { display: flex; @@ -65,12 +66,12 @@ } .btn--yellow:hover { - background: #e0a71f !important; + background: var(--color-yellow-hover) !important; border: 4px solid var(--color-white) !important; } .btn--yellow:active { - background: #c99319 !important; + background: var(--color-yellow-active) !important; } .btn--yellow:focus { @@ -109,7 +110,7 @@ /* Button Styles - Red (Delete API key) */ .btn--red { - background: #bf6366 !important; + background: var(--color-red) !important; border: 4px solid var(--color-white) !important; color: var(--color-white) !important; font-family: Heebo, sans-serif !important; @@ -121,12 +122,12 @@ } .btn--red:hover { - background: #a65558 !important; + background: var(--color-red-hover) !important; border: 4px solid var(--color-white) !important; } .btn--red:active { - background: #8d484b !important; + background: var(--color-red-active) !important; } .btn--red:focus { @@ -136,7 +137,7 @@ } /* Button Styles - Green (Logout) */ -.btn--green{ +.btn--green { background: var(--color-sherpa800) !important; border: 4px solid var(--color-white) !important; color: var(--color-white) !important; @@ -151,7 +152,7 @@ .btn--green:hover { border-width: 2px !important; padding: 15px 25px !important; - background: rgba(255, 255, 255, 0.1) !important; + background: var(--color-white-transparent) !important; } .btn--green:active { @@ -176,7 +177,7 @@ /* Headings */ h3 { color: var(--color-white); - font-family: "Exo 2", sans-serif; + font-family: 'Exo 2', sans-serif; font-size: 28px; font-weight: 600; line-height: 32px; @@ -184,110 +185,6 @@ h3 { text-align: center; } -/* Card Styles */ -.p-card { - background: var(--color-sherpa800) !important; - border: 1px solid var(--color-sherpa600) !important; - border-radius: 8px !important; - box-shadow: none !important; -} - -.p-card .p-card-body { - padding: 32px !important; - color: var(--color-white) !important; -} - -.p-card .p-card-content { - color: var(--color-white) !important; - font-family: 'Heebo', sans-serif !important; - font-size: 16px !important; - line-height: 24px !important; - min-height: 200px; -} - -/* DataTable Styles */ -.p-datatable { - background: var(--color-sherpa800) !important; - border: 1px solid var(--color-sherpa600) !important; - border-radius: 8px !important; - overflow: hidden !important; - width: 100% !important; - table-layout: fixed !important; -} - -.p-datatable .p-datatable-table { - width: 100% !important; - table-layout: fixed !important; -} - -.p-datatable .p-datatable-header { - background: var(--color-sherpa700) !important; - border-bottom: 1px solid var(--color-sherpa600) !important; - color: var(--color-white) !important; - padding: 16px !important; -} - -.p-datatable .p-datatable-thead > tr > th { - background: var(--color-sherpa700) !important; - border: 1px solid var(--color-sherpa600) !important; - color: var(--color-white) !important; - font-family: 'Exo 2', sans-serif !important; - font-size: 16px !important; - font-weight: 600 !important; - padding: 16px !important; - box-sizing: border-box !important; -} - -.p-datatable .p-datatable-tbody > tr { - background: var(--color-sherpa800) !important; - color: var(--color-white) !important; -} - -.p-datatable .p-datatable-tbody > tr > td { - border: 1px solid var(--color-sherpa600) !important; - padding: 16px !important; - font-family: 'Heebo', sans-serif !important; - font-size: 16px !important; - overflow-wrap: break-word !important; - word-wrap: break-word !important; - box-sizing: border-box !important; -} - -.p-datatable .p-datatable-tbody > tr:hover { - background: rgba(255, 255, 255, 0.05) !important; -} - -/* Empty message in DataTable */ -.p-datatable .p-datatable-emptymessage > td { - color: var(--color-sherpa400) !important; -} - -/* DataTable Column Widths */ - .p-datatable .p-datatable-thead > tr > th:nth-child(1), - .p-datatable .p-datatable-tbody > tr > td:nth-child(1), - .p-datatable .route-column { - width: 60% !important; - } - - .p-datatable .p-datatable-thead > tr > th:nth-child(2), - .p-datatable .p-datatable-tbody > tr > td:nth-child(2), - .p-datatable .limits-column { - width: 40% !important; - } - -/* Info Panel Styling */ -#infoPanel { - color: var(--color-white); - font-family: 'Heebo', sans-serif; - font-size: 16px; - line-height: 24px; - padding: 24px; - text-align: left; - white-space: pre-line; - overflow-wrap: break-word; - word-wrap: break-word; -} - /* Uniform icons */ .icon { width: 20px; @@ -315,31 +212,97 @@ h3 { text-decoration: underline; } -/* Routes Information */ -.routes-info-box { +/* Login */ +.login-screen { + text-align: center; + padding: 60px 20px; +} + +.login-screen h3 { + margin-bottom: 24px; +} + +.login-screen-text { + color: var(--color-sherpa200); + margin-bottom: 32px; + font-size: 18px; + line-height: 24px; +} + +.login-screen-docs { + margin-top: 24px; +} + +/* API Key Container */ +.api-key-container { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; background-color: var(--color-sherpa700); - border: 1px solid var(--color-sherpa500); border-radius: 8px; - padding: 12px 16px; - margin-bottom: 16px; - font-size: 14px; - color: var(--color-sherpa200); + border: 1px solid var(--color-sherpa600); +} + +.api-key-text { + font-family: monospace; + font-size: 16px; + font-style: normal; + color: var(--color-white); + flex: 1; + word-break: break-all; +} + +/* API Key Label */ +.api-key-label { + margin-bottom: 12px; + font-size: 16px; + font-weight: 600; + font-style: normal; + color: var(--color-white); +} + +.btn-copy { + background: var(--color-sherpa600) !important; + border: 2px solid var(--color-sherpa500) !important; + color: var(--color-white) !important; + padding: 8px !important; + border-radius: 4px !important; + min-width: 40px !important; + width: 40px !important; + height: 40px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + transition: all 0.3s ease !important; + cursor: pointer; +} + +.btn-copy:hover { + background: var(--color-sherpa500) !important; + border-color: var(--color-sherpa400) !important; } -.routes-info-box .info-item { - margin-bottom: 8px; +.btn-copy:active { + background: var(--color-sherpa700) !important; } -.routes-info-box .info-item:last-child { - margin-bottom: 0; +.btn-copy:focus { + border-color: var(--color-yellow) !important; + outline: none !important; +} + +.copy-icon { + width: 20px; + height: 20px; + display: block; } /* Tablet layout (600px - 1023px) */ @media (min-width: 600px) and (max-width: 1023px) { .App { - background-image: - linear-gradient(180deg, rgba(0, 0, 0, 0.30) 0%, rgba(0, 0, 0, 0.00) 15.58%), - radial-gradient(68.05% 68.05% at 50.79% 4.4%, rgba(12, 59, 65, 0.30) 0%, #0C3B41 100%), + background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 15.58%), + radial-gradient(68.05% 68.05% at 50.79% 4.4%, rgba(12, 59, 65, 0.3) 0%, #0c3b41 100%), url('/background-image-tablet.jpg'); } @@ -352,39 +315,24 @@ h3 { line-height: 28px; } - .p-card .p-card-body { - padding: 24px !important; - } - - .p-datatable .p-datatable-thead > tr > th:nth-child(1), - .p-datatable .p-datatable-tbody > tr > td:nth-child(1), - .p-datatable .route-column { - width: 55% !important; - } - - .p-datatable .p-datatable-thead > tr > th:nth-child(2), - .p-datatable .p-datatable-tbody > tr > td:nth-child(2), - .p-datatable .limits-column { - width: 45% !important; - } - .btn--uniform { min-width: 160px !important; } - .routes-info-box { - font-size: 13px; - padding: 10px 14px; + .login-screen { + padding: 48px 20px; + } + + .login-screen-text { + font-size: 16px; } } /* Mobile layout (up to 599px) */ @media (max-width: 599px) { .App { - background-image: - linear-gradient(180deg, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.00) 30%), - radial-gradient(90% 90% at 50% 10%, rgba(12, 59, 65, 0.50) 0%, #0C3B41 100%), - url('/background-image-mobile.jpg'); + background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0) 30%), + radial-gradient(90% 90% at 50% 10%, rgba(12, 59, 65, 0.5) 0%, #0c3b41 100%), url('/background-image-mobile.jpg'); } .content-container { @@ -398,22 +346,6 @@ h3 { margin: 24px 0 16px 0; } - .p-card .p-card-body { - padding: 20px !important; - } - - .p-datatable .p-datatable-thead > tr > th:nth-child(1), - .p-datatable .p-datatable-tbody > tr > td:nth-child(1), - .p-datatable .route-column { - width: 50% !important; - } - - .p-datatable .p-datatable-thead > tr > th:nth-child(2), - .p-datatable .p-datatable-tbody > tr > td:nth-child(2), - .p-datatable .limits-column { - width: 50% !important; - } - .btn--uniform { min-width: 100% !important; width: 100% !important; @@ -423,8 +355,16 @@ h3 { font-size: 14px; } - .routes-info-box { - font-size: 12px; - padding: 8px 12px; + .login-screen { + padding: 40px 16px; + } + + .login-screen-text { + font-size: 16px; + margin-bottom: 24px; } -} \ No newline at end of file + + .login-screen-docs { + margin-top: 20px; + } +} diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 9c9e91c..48512fd 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -1,19 +1,18 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import './App.css'; import 'primereact/resources/themes/lara-light-indigo/theme.css'; import 'primereact/resources/primereact.min.css'; import '/node_modules/primeflex/primeflex.css'; import { Button } from 'primereact/button'; -import { Card } from 'primereact/card'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; +import { useAuth } from 'react-oidc-context'; +import { toast } from 'react-toastify'; import Header from './components/Header'; import Footer from './components/Footer'; - +import InfoPanelCard from './components/InfoPanelCard'; import { getAPIKey, deleteAPIKey, getRoutes } from './Services/apiService'; -import { useAuth } from 'react-oidc-context'; -import { toast } from 'react-toastify'; function App() { const auth = useAuth(); @@ -33,12 +32,40 @@ function App() { }; }, [auth]); + const handleCopyApiKey = (apiKey) => { + navigator.clipboard.writeText(apiKey); + toast.success('API key copied to clipboard!', { + position: 'top-right', + autoClose: 3000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + theme: 'dark', + }); + }; + const handleGetAPIKey = async () => { try { const { data, isError } = await getAPIKey(); if (!isError) { const apiKey = data.apiKey; - setInfoMessage(`API key:\n ${apiKey}`); + setInfoMessage( + <> +
API Key
+- Please log in to access your API keys and manage routes -
-