├── .github ├── actions │ ├── bundle │ │ ├── Dockerfile │ │ ├── action.yml │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ ├── commenter │ │ ├── Dockerfile │ │ ├── action.yml │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json │ └── coverage-report │ │ ├── Dockerfile │ │ ├── action.yml │ │ ├── index.js │ │ ├── package-lock.json │ │ └── package.json └── workflows │ ├── git-commit-lint.yml │ ├── node-deploy-storybook.yml │ ├── node-pr-jobs.yml │ └── stale-issues.yml ├── .gitignore ├── .prettierignore ├── .storybook ├── main.js └── preview.js ├── CODE_OF_CONDUCT.md ├── GOVERNANCE.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── __mocks__ ├── README.md └── react-i18next.tsx ├── build ├── README.md ├── dockerfile ├── webpack.client.dev.js ├── webpack.client.prod.js ├── webpack.common.js └── webpack.server.prod.js ├── client ├── Bootstrap │ ├── GraphQLClient │ │ ├── GraphQLClient.ts │ │ └── index.ts │ ├── Navigation │ │ ├── README.md │ │ ├── index.ts │ │ ├── types.ts │ │ └── useRouteConfig │ │ │ ├── README.md │ │ │ ├── useRouteConfig.assets.tsx │ │ │ ├── useRouteConfig.hook.ts │ │ │ ├── useRouteConfig.spec.tsx │ │ │ └── useRouteConfig.types.ts │ ├── README.md │ ├── index.html │ └── index.tsx ├── Contexts │ ├── ConfigFeatureFlag │ │ ├── ConfigFeatureFlag.assets.ts │ │ ├── ConfigFeatureFlag.spec.tsx │ │ ├── ConfigFeatureFlag.types.ts │ │ ├── Context.tsx │ │ ├── FeatureFlag.spec.tsx │ │ ├── FeatureFlag.view.tsx │ │ ├── README.md │ │ └── index.ts │ ├── Introspect │ │ ├── ExpectationTypes.ts │ │ ├── Introspection.spec.ts │ │ ├── Introspection.ts │ │ ├── README.md │ │ └── mock │ │ │ ├── expectations-basic.ts │ │ │ ├── expectations-empty-topic.ts │ │ │ ├── expectations-missing-mutation.ts │ │ │ ├── expectations-missing-type.ts │ │ │ ├── mock-entities-empty-topic.ts │ │ │ ├── mock-entities.ts │ │ │ ├── mock-introspection-missing-mutation-block.ts │ │ │ ├── mock-introspection-missing-mutation-type.ts │ │ │ ├── mock-introspection-non-object-type.ts │ │ │ ├── mock-introspection-unindexable.ts │ │ │ ├── mock-introspection-unsupported-field-type.ts │ │ │ ├── mock-introspection-wrong-type-on-mutation-block.ts │ │ │ ├── mock-introspection-wrong-type-on-topic.ts │ │ │ └── mock-introspection.ts │ ├── Logging │ │ ├── Context.tsx │ │ ├── Logging.types.ts │ │ ├── README.md │ │ └── index.ts │ ├── README.md │ ├── index.ts │ └── types.ts ├── Elements │ └── README.md ├── Groups │ └── README.md ├── Hooks │ ├── README.md │ ├── index.ts │ ├── useConfigFeatureFlag │ │ ├── README.md │ │ ├── useConfigFeatureFlag.spec.ts │ │ └── useConfigFeatureFlag.ts │ └── useLogger │ │ ├── Hook.ts │ │ ├── README.md │ │ ├── index.ts │ │ ├── useLogger.feature │ │ └── useLogger.steps.tsx ├── Images │ ├── README.md │ ├── favicon.ico │ ├── index.d.ts │ └── logo.png ├── Pages │ └── README.md ├── Panels │ ├── Error404 │ │ ├── Error404.feature │ │ ├── Error404.steps.tsx │ │ ├── Error404.tsx │ │ └── index.ts │ ├── Home │ │ ├── Home.feature │ │ ├── Home.steps.tsx │ │ ├── Home.tsx │ │ ├── index.ts │ │ └── style.scss │ ├── README.md │ └── index.ts ├── Queries │ ├── Config │ │ └── index.ts │ └── README.md ├── README.md ├── Utils │ ├── README.md │ ├── index.ts │ ├── sanitise │ │ ├── README.md │ │ ├── sanitise.spec.ts │ │ └── sanitise.ts │ └── window │ │ ├── README.md │ │ ├── window.spec.ts │ │ └── window.ts ├── i18n │ ├── README.md │ ├── index.ts │ └── locale │ │ ├── de.json │ │ ├── en.json │ │ └── index.ts ├── jest.config.js └── tsconfig.json ├── commitlint.config.js ├── config ├── README.md ├── config.types.ts ├── configHelpers.ts ├── featureflags.ts ├── index.ts ├── runtime.ts ├── static.ts └── types.ts ├── cypress-cucumber-preprocessor.config.js ├── docs ├── Architecture.md ├── Build.md ├── Coding.md ├── ContinuousIntegration.md ├── Contribution.md ├── Linting.md ├── README.md ├── Test.md └── assets │ ├── DevelopmentTopology.png │ ├── DevelopmentTopologyUsingRealStrimziAdmin.png │ ├── EndToEndTopology.png │ ├── ProductionTopology.png │ └── StrimziUIDiagrams.pptx ├── e2e ├── README.md ├── cypress-cucumber-preprocessor.config.js ├── cypress.json ├── features │ ├── README.md │ ├── coreUX.feature │ └── step_definitions │ │ └── coreUX.stepdef.ts ├── plugins │ ├── README.md │ └── index.ts ├── support │ ├── README.md │ └── index.ts └── tsconfig.json ├── husky.config.js ├── linting ├── .eslintignore ├── .prettierignore ├── README.md ├── commitlint.config.js ├── eslint.config.js ├── husky.config.js ├── license-check-and-add.config.json ├── lint-staged.config.js ├── prettier.config.js └── stylelint.config.js ├── package-lock.json ├── package.json ├── server ├── README.md ├── api │ ├── README.md │ ├── api.feature │ ├── api.steps.ts │ ├── controller.ts │ ├── index.ts │ └── router.ts ├── client │ ├── README.md │ ├── client.feature │ ├── client.steps.ts │ ├── controller.ts │ ├── index.ts │ └── router.ts ├── config │ ├── README.md │ ├── config.feature │ ├── config.steps.ts │ ├── controller.ts │ ├── index.ts │ └── router.ts ├── core │ ├── README.md │ ├── app.ts │ ├── core.feature │ ├── core.steps.ts │ ├── index.ts │ └── modules.ts ├── jest.config.js ├── log │ ├── README.md │ ├── index.ts │ ├── log.feature │ ├── log.steps.ts │ └── router.ts ├── logging.ts ├── main.ts ├── mockapi │ ├── README.md │ ├── data.ts │ ├── index.ts │ ├── mockapi.feature │ ├── mockapi.steps.ts │ └── router.ts ├── placeholderFunctionsToReplace.ts ├── serverConfig.ts ├── test_common │ ├── __test_fixtures__ │ │ ├── README.md │ │ ├── client │ │ │ ├── images │ │ │ │ └── picture.svg │ │ │ ├── index.html │ │ │ └── protected.html │ │ ├── main.http.conf.js │ │ └── main.https.conf.js │ ├── commonServerSteps.ts │ ├── testConfigs.ts │ └── testGQLRequests.ts ├── tsconfig.json └── types.ts ├── test_common ├── README.md ├── jest.common.config.js ├── jest_cucumber_support │ ├── README.md │ ├── commonStepdefs.ts │ ├── commonTestTypes.ts │ └── index.ts ├── jest_rtl_setup.ts └── mockfile.util.ts ├── tsconfig.base.json └── utils ├── README.md ├── dev_config ├── README.md ├── mockadmin.config.js ├── req.conf ├── server.dev.config.js └── server.e2e.config.js ├── test ├── README.md ├── context │ ├── README.md │ └── context.util.tsx ├── i18n │ ├── README.md │ └── i18n.util.ts ├── index.ts └── withApollo │ ├── README.md │ └── withApollo.util.tsx └── tooling ├── README.md ├── aliasHelper.js ├── constants.js ├── generateCerts.sh ├── headers ├── README.md └── StrimziHeader.txt └── runtimeDevUtils.js /.github/actions/bundle/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | FROM node:lts-alpine 4 | 5 | COPY index.js . 6 | COPY package.json . 7 | COPY package-lock.json . 8 | 9 | RUN npm install 10 | 11 | ENTRYPOINT ["node", "/index.js"]; 12 | -------------------------------------------------------------------------------- /.github/actions/bundle/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | name: "Bundle size" 4 | description: "Check the size of the bundle" 5 | inputs: 6 | CLIENT_MASTER_REPORT: 7 | description: "Bundle report from master branch for client code" 8 | SERVER_MASTER_REPORT: 9 | description: "Bundle report from master branch for server code" 10 | outputs: 11 | bundle_report: 12 | description: "Markdown representation of bundle sizes" 13 | overall_bundle_size_change: 14 | description: "Overall percentage change of bundle sizes between master and current" 15 | runs: 16 | using: "docker" 17 | image: "Dockerfile" 18 | -------------------------------------------------------------------------------- /.github/actions/bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundleSize", 3 | "version": "1.0.0", 4 | "description": "Script to check the size of a bundle", 5 | "main": "main.js", 6 | "scripts": {}, 7 | "author": "", 8 | "license": "Apache 2.0", 9 | "dependencies": { 10 | "@actions/core": "^1.2.4", 11 | "@actions/github": "^2.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/actions/commenter/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | FROM node:lts-alpine 4 | 5 | COPY index.js . 6 | COPY package.json . 7 | COPY package-lock.json . 8 | 9 | RUN npm install 10 | 11 | ENTRYPOINT ["node", "/index.js"]; 12 | -------------------------------------------------------------------------------- /.github/actions/commenter/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | name: "Commenter" 4 | description: "Comment inputed text on issue/pull request" 5 | inputs: 6 | GITHUB_TOKEN: 7 | description: "Github action bot token" 8 | BUNDLE_REPORT: 9 | description: "Markdown representation of bundle sizes" 10 | OVERALL_BUNDLE_SIZE_CHANGE: 11 | description: "Overall percentage change of bundle sizes between master and current" 12 | TEST_COVERAGE_CLIENT: 13 | description: "Test coverage report from jest tests for client code" 14 | TEST_COVERAGE_SERVER: 15 | description: "Test coverage report from jest tests for server code" 16 | runs: 17 | using: "docker" 18 | image: "Dockerfile" 19 | -------------------------------------------------------------------------------- /.github/actions/commenter/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const core = require("@actions/core"); 6 | const github = require("@actions/github"); 7 | 8 | const pull_request_number = github.context.payload.pull_request.number; 9 | const repo = github.context.repo; 10 | const github_token = core.getInput("GITHUB_TOKEN"); 11 | const octokit = new github.GitHub(github_token); 12 | 13 | async function getCommentID() { 14 | let { data: comments } = await octokit.issues.listComments({ 15 | ...repo, 16 | issue_number: pull_request_number, 17 | }); 18 | 19 | let res = comments.filter((comment) => { 20 | return comment.user.login === "github-actions[bot]"; 21 | }); 22 | 23 | if (res.length > 0) { 24 | return res[0].id; 25 | } else { 26 | return null; 27 | } 28 | } 29 | 30 | async function comment(message) { 31 | try { 32 | const commentID = await getCommentID(); 33 | 34 | if (commentID) { 35 | octokit.issues.updateComment({ 36 | ...repo, 37 | issue_number: pull_request_number, 38 | comment_id: commentID, 39 | body: message, 40 | }); 41 | } else { 42 | octokit.issues.createComment({ 43 | ...repo, 44 | issue_number: pull_request_number, 45 | body: message, 46 | }); 47 | } 48 | } catch (error) { 49 | core.setFailed(error.message); 50 | } 51 | } 52 | 53 | async function createComment() { 54 | try { 55 | const bundleReportContent = core.getInput("BUNDLE_REPORT") ? JSON.parse(core.getInput("BUNDLE_REPORT")) : {}; 56 | const testCoverageClient = core.getInput("TEST_COVERAGE_CLIENT"); 57 | const testCoverageServer = core.getInput("TEST_COVERAGE_SERVER"); 58 | 59 | const title = '# PR Report'; 60 | const bundleText = `## Bundle Sizes\n${Object.entries(bundleReportContent).reduce((acc, [codeArea, {bundle_report, overall_bundle_size_change}]) => `${acc}
View bundle sizes for '${codeArea}'
\n\n${bundle_report}\n##### Overall bundle size change: ${overall_bundle_size_change}\n
`, '')}`; 61 | const testText = `## Test Coverage\n
View test coverage
\n\n${testCoverageClient}\n\n\n${testCoverageServer}\n
`; 62 | const footer = `Triggered by commit: ${github.context.sha}`; 63 | 64 | const commentText = [title, bundleText, testText, footer]; 65 | 66 | await comment(commentText.join('\n\n')); 67 | } catch (error) { 68 | core.setFailed(error.message); 69 | } 70 | } 71 | 72 | createComment(); 73 | -------------------------------------------------------------------------------- /.github/actions/commenter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commenter", 3 | "version": "1.0.0", 4 | "description": "Action to comment inputed text", 5 | "main": "main.js", 6 | "scripts": {}, 7 | "author": "", 8 | "license": "Apache 2.0", 9 | "dependencies": { 10 | "@actions/core": "^1.2.4", 11 | "@actions/github": "^2.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/actions/coverage-report/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | FROM node:lts-alpine 4 | 5 | COPY index.js . 6 | COPY package.json . 7 | COPY package-lock.json . 8 | 9 | RUN npm install 10 | 11 | ENTRYPOINT ["node", "/index.js"]; 12 | -------------------------------------------------------------------------------- /.github/actions/coverage-report/action.yml: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | name: "Coverage report" 4 | description: "Report test coverage" 5 | outputs: 6 | test_coverage_client: 7 | description: "Text for test coverage on client code" 8 | test_coverage_server: 9 | description: "Text for test coverage on server code" 10 | runs: 11 | using: "docker" 12 | image: "Dockerfile" 13 | -------------------------------------------------------------------------------- /.github/actions/coverage-report/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const core = require("@actions/core"); 6 | const fs = require("fs"); 7 | 8 | async function formatCoverage(testType) { 9 | try { 10 | const coverage = JSON.parse( 11 | fs.readFileSync(`./coverage/${testType}/coverage-summary.json`) 12 | ); 13 | 14 | core.setOutput(`test_coverage_${testType}`, coverageTable(coverage)); 15 | } catch (error) { 16 | core.setFailed(error.message); 17 | } 18 | } 19 | 20 | const coverageTable = (coverage) => { 21 | let coverageText = 22 | "| File | Lines | Statement | Functions | Branches |\n| --- | --- | --- | --- | --- |\n"; 23 | 24 | const regex = /^(.+)strimzi-ui\/(.+)$/; 25 | 26 | coverageText += Object.entries(coverage).reduce((text, [key, value]) => { 27 | console.log(key); 28 | 29 | return `${text}| ${key != "total" ? key.match(regex)[2] : "Total"} | ${ 30 | value.lines.pct 31 | }% | ${value.statements.pct}% | ${value.functions.pct}% | ${ 32 | value.branches.pct 33 | }% |\n`; 34 | }, ''); 35 | 36 | return coverageText; 37 | }; 38 | 39 | return Promise.all([formatCoverage("client"), 40 | formatCoverage("server")]); 41 | -------------------------------------------------------------------------------- /.github/actions/coverage-report/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coverage-report", 3 | "version": "1.0.0", 4 | "description": "Script to get coverage report comment results", 5 | "main": "main.js", 6 | "scripts": {}, 7 | "author": "", 8 | "license": "Apache 2.0", 9 | "dependencies": { 10 | "@actions/core": "^1.2.4", 11 | "@actions/github": "^2.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/git-commit-lint.yml: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | name: Git 4 | 5 | on: [pull_request] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-20.04 10 | name: Lint Commit Messages 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | ref: ${{ github.event.pull_request.head.sha }} 16 | - name: Use Node.js 14.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 14.x 20 | - name: Fetch origin 21 | run: git fetch origin 22 | - name: Event Sender 23 | run: echo ${{ github.event.sender.login }} 24 | - name: Lint commit message 25 | run: npx commitlint --from origin/${{ github.base_ref }} --to HEAD 26 | -------------------------------------------------------------------------------- /.github/workflows/node-deploy-storybook.yml: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | name: Storybook 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-20.04 13 | name: Deploy Storybook 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js 14.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14.x 21 | - name: Install Dependencies 22 | run: npm ci 23 | - name: Install Storybook 24 | run: npm install @storybook/storybook-deployer @storybook/react -g 25 | - name: Deploy storybook 26 | run: storybook-to-ghpages -o .out --ci 27 | env: 28 | GH_TOKEN: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/node-pr-jobs.yml: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | name: Node 4 | 5 | on: [pull_request] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-20.04 10 | name: Linter 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 14.x 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 14.x 17 | - name: Install Dependencies 18 | run: npm ci 19 | - name: Lint all files 20 | run: npm run lint 21 | 22 | e2e-tests: 23 | runs-on: ubuntu-20.04 24 | name: E2E Tests 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: 14.x 30 | - name: Install dependencies 31 | run: npm ci 32 | - name: run tests 33 | run: npm run test:e2e 34 | - uses: actions/upload-artifact@v2 35 | if: failure() 36 | with: 37 | name: failures 38 | path: failure_output 39 | 40 | storybook-artifact: 41 | runs-on: ubuntu-20.04 42 | name: PR Storybook 43 | 44 | steps: 45 | - uses: actions/checkout@v2 46 | - name: Use Node.js 14.x 47 | uses: actions/setup-node@v1 48 | with: 49 | node-version: 14.x 50 | - name: Install Dependencies 51 | run: npm ci 52 | - name: Build Storybook 53 | run: npm run storybook:build 54 | - name: Upload Storybook Artifact 55 | uses: actions/upload-artifact@v2 56 | with: 57 | name: storybook 58 | path: .out 59 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | name: Close stale issues 4 | on: 5 | schedule: 6 | - cron: "30 1 * * *" 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/stale@v3 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' 16 | days-before-stale: 30 17 | days-before-close: 7 18 | exempt-issue-labels: 'work-in-progress' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | # dependancies 4 | node_modules/ 5 | 6 | # built content 7 | dist/ 8 | .out/ 9 | js/ 10 | 11 | # test/supporting generated content 12 | generated/ 13 | coverage/ 14 | e2e/failure_output 15 | .out/ 16 | 17 | # misc 18 | .history 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .npmrc 25 | .eslintcache 26 | 27 | #IDEs 28 | .vscode/ 29 | .idea/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | linting/.prettierignore -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | const devWebpackConfig = require('../build/webpack.client.dev.js'); 7 | 8 | module.exports = { 9 | stories: ['../client/**/*.stories.@(js|jsx|ts|tsx)'], 10 | addons: [ 11 | '@storybook/addon-essentials', 12 | '@storybook/addon-knobs', 13 | '@storybook/addon-actions', 14 | '@storybook/addon-storysource', 15 | '@storybook/addon-links', 16 | 'storybook-readme', 17 | ], 18 | webpackFinal: (storybookWebpackConfig) => ({ 19 | // Merge dev webpack config and storybook webpack config, with storybook 20 | // taking precedence, and ensuring module rules, plugins and resolve aliases 21 | // include the dev webpack settings. 22 | ...devWebpackConfig, 23 | ...storybookWebpackConfig, 24 | module: { 25 | rules: [ 26 | ...devWebpackConfig.module.rules, 27 | ...storybookWebpackConfig.module.rules, 28 | ], 29 | }, 30 | resolve: { 31 | ...storybookWebpackConfig.resolve, 32 | alias: { 33 | ...devWebpackConfig.resolve.alias, 34 | ...storybookWebpackConfig.resolve.alias 35 | }, 36 | } 37 | }), 38 | }; 39 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export const parameters = { 6 | actions: { argTypesRegex: '^on[A-Z].*' }, 7 | }; 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Strimzi Community Code of Conduct 2 | 3 | Strimzi Community Code of Conduct is defined in the [governance repository](https://github.com/strimzi/governance/blob/master/CODE_OF_CONDUCT.md). 4 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # Strimzi Governance 2 | 3 | Strimzi Governance is defined in the [governance repository](https://github.com/strimzi/governance/blob/master/GOVERNANCE.md). 4 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Strimzi Maintainers list 2 | 3 | Strimzi Maintainers list is defined in the [governance repository](https://github.com/strimzi/governance/blob/master/MAINTAINERS). 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 2 | [![Twitter Follow](https://img.shields.io/twitter/follow/strimziio.svg?style=social&label=Follow&style=for-the-badge)](https://twitter.com/strimziio) 3 | 4 | # Strimzi UI 5 | 6 | This repository contains the Strimzi UI and its implementation. 7 | Strimzi UI provides a way for managing Strimzi and Kafka clusters (+ other components) deployed by it using a graphical user interface. 8 | 9 | This UI is currently not in a state where it can be used. Is it still early on in it's development but we hope to have something usable very soon! If you're interested in what we're working on, please [view our project board](https://github.com/orgs/strimzi/projects/2). If you're interested in contributing, please [view our contribution guidelines]('./docs/Condribution.md). 10 | 11 | ## Getting started 12 | 13 | This UI uses `npm` to provide dependency management. If you wish to develop the UI, you will need: 14 | 15 | - [npm version 6.14.8 or later](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 16 | - [node 14.15.0 or later](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 17 | 18 | Once these prerequisites are met, all required dependencies to build and run the UI can be downloaded by running the following command: 19 | 20 | ``` 21 | npm install 22 | ``` 23 | 24 | The Strimzi-UI can be developed while making use of TLS certificates between server and client, as would be the case in a typical production deployment. The [`openssl`](https://www.openssl.org/) tool and configuration (used via the `npm run addDevCerts` command) can generate representative development time certificates for this purpose, given `openssl` is installed for your operating system. This is not required however to develop the UI. 25 | 26 | If you run into any issues while working in this repo, please check out [the troubleshooting guide](#troubleshooting). 27 | 28 | ### Helpful commands 29 | 30 | `npm` scripts are provided for common tasks. These include: 31 | 32 | - `npm run test` - runs all tests for the client and server 33 | - `npm run start` - runs the UI client and server in development mode 34 | - `npm run addDevCerts` - requires `openssl`, will generate certificates for development purposes for UI development 35 | - `npm run build` - builds the UI 36 | - `npm run clean` - deletes the build/generated content directories 37 | - `npm run lint` - lints the codebase. See [`Linting`](./docs/Linting.md) for the individual linting steps 38 | - `npm run storybook` - runs [Storybook](./docs/Architecture.md#storybook) for the UI components. 39 | 40 | ## Implementation documentation 41 | 42 | Further details around how this UI is implemented can be found below: 43 | 44 | - [Architecture](./docs/Architecture.md) 45 | - [Build](./docs/Build.md) 46 | - [Linting](./docs/Linting.md) 47 | - [Test](./docs/Test.md) 48 | - [Contribution](./docs/Contribution.md) 49 | - [Coding Standards](./docs/Coding.md) 50 | 51 | ## Troubleshooting 52 | 53 | Currently there are no known issues. 54 | -------------------------------------------------------------------------------- /__mocks__/README.md: -------------------------------------------------------------------------------- 1 | ## Contents 2 | 3 | Manual mocks for node modules in Jest. 4 | 5 | | module | behaviour | 6 | | ------------- | ------------------------------------------------------------------------------------------------------- | 7 | | react-i18next | stubs out `useTranslation` hook and `` to provide stringified representations of the translation | 8 | -------------------------------------------------------------------------------- /__mocks__/react-i18next.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { translate, translateWithFormatting } from '../utils/test'; 6 | import { TFunction } from 'i18next'; 7 | import React, { FunctionComponent } from 'react'; 8 | import { UseTranslationResponse } from 'react-i18next'; 9 | 10 | const i18next: jest.Mock = jest.createMockFromModule('react-i18next'); 11 | 12 | const useTranslation = () => { 13 | const trans = translate as TFunction; 14 | 15 | const hookResult = [translate, null, true]; 16 | // eslint-disable-next-line id-length 17 | (hookResult as UseTranslationResponse).t = trans; 18 | 19 | return hookResult; 20 | }; 21 | 22 | const Trans: FunctionComponent<{ i18nKey: string }> = ({ 23 | i18nKey, 24 | children, 25 | }) =>
{translateWithFormatting(i18nKey, children)}
; 26 | 27 | module.exports = { ...i18next, useTranslation, Trans }; 28 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | This directory contains code relating to the building of the UI at dev and production build time. [Full details on how the build has been set up can be found here.](../docs/Build.md). The intent is as much configuration is shared as possible from `webpack.common.js`, and the function it exports, `returnBasicConfigMergedWith`. This allows the various build modes to define (in an additive manner) just the configuration they need to, thus keeping the configuration purposeful/readable both in that file, and in common. If a new build mode is added, it should follow the form set by the `webpack.dev.js` and `webpack.prod.js`. 4 | 5 | ## Files 6 | 7 | - `webpack.common.js` - contains common webpack configuration and helper functions. These are: 8 | - `returnBasicConfigMergedWith` - a function which returns common configuration options. Any of these can be overridden if required. Check the file for parameters/usage. 9 | - `moduleLoaders` - an object containing functions that will return module loading rules. These should be called when defining a build mode's `module.rules` value. The functions provided allow for custom values to be provided - see `webpack.common.js` for usage. Any common loaders should be provided/used this way, as it allows easy modification/extension, while keeping a sensible set of default values. 10 | - `plugins` - an object which contains functions which return the various plugins used across more than one build mode, with a common minimum configuration. These should be imported and called in a build mode's `plugins` array, with custom config passed to suit that mode. Any common plugins should be provided/used this way, as it allows easy modification/extension, while keeping a sensible set of default values. 11 | - `CONSTANTS` - an object which contains useful values for use in any build mode 12 | - `webpack.client.dev.js` - webpack development build mode. Stands up a configured instance of `webpack-dev-server` to supplement the common config/to enable faster development. 13 | - `webpack.client.prod.js` - webpack production build mode for client code. Contains configuration to minify and compress all built output. 14 | - `webpack.server.prod.js` - webpack production build mode for server code. Contains configuration to minify and compress all built output. 15 | - `babelPresets.js` - babel transpiling configuration. See comments in the file which detail what options/plugins are in use. 16 | - `dockerfile` - a dockerfile which builds the Strimzi-ui, and when run hosts it on port 3000 17 | -------------------------------------------------------------------------------- /build/dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Strimzi authors. 3 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | # 5 | # Note: this is provided for purposes of completing/illustrating a production build of the UI - this will be replaced in due course with a strimzi based image + node runtime w/ tini etc 6 | 7 | FROM node:lts-alpine as builder 8 | 9 | WORKDIR /usr/strimzi-ui/ 10 | 11 | COPY --chown=root:root / /usr/strimzi-ui/ 12 | 13 | # install all deps, and build client/server 14 | RUN npm install -q \ 15 | && npm run build 16 | # clear deps, and only install prod deps - needed as server build does not bundle them/node provided functions 17 | RUN rm -rf /node_modules \ 18 | && npm install -q --only=production \ 19 | && npm dedupe 20 | 21 | # ---------------------------------------------------------------------------- # 22 | 23 | FROM node:lts-alpine 24 | 25 | # copy required built output to image 26 | COPY --from=builder --chown=root:root /usr/strimzi-ui/LICENSE ./ 27 | COPY --from=builder --chown=root:root /usr/strimzi-ui/dist ./dist 28 | COPY --from=builder --chown=root:root /usr/strimzi-ui/node_modules ./node_modules 29 | 30 | USER node 31 | 32 | EXPOSE 3000 33 | 34 | ENTRYPOINT [ "node" , "/dist/server/main.js" ] -------------------------------------------------------------------------------- /build/webpack.client.dev.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | // client development specific plugins and webpack configuration 6 | 7 | const { 8 | returnBasicConfigMergedWith, 9 | plugins, 10 | moduleLoaders, 11 | CONSTANTS, 12 | } = require('./webpack.common.js'); 13 | 14 | const { 15 | devEnvToUseTls, 16 | devEnvValues, 17 | } = require('../utils/tooling/runtimeDevUtils.js'); 18 | const { webpackDevServer, devServer } = devEnvValues; 19 | 20 | const { 21 | withHTMLPlugin, 22 | withNormalModuleReplacementPlugin, 23 | withMiniCssExtractPlugin, 24 | withWebpackBundleAnalyzerPlugin, 25 | withTsconfigPathsPlugin, 26 | } = plugins; 27 | const { 28 | withStylingModuleLoader, 29 | withFontModuleLoader, 30 | withImageModuleLoader, 31 | withTypeScriptModuleLoader, 32 | } = moduleLoaders; 33 | const { DEVELOPMENT, BUILD_DIR, BUNDLE_ANALYSER_DIR } = CONSTANTS; 34 | 35 | const devSpecificConfig = { 36 | mode: DEVELOPMENT, 37 | devtool: 'eval-source-map', 38 | module: { 39 | rules: [ 40 | withStylingModuleLoader(['style-loader']), 41 | withTypeScriptModuleLoader('../client/tsconfig.json'), 42 | withFontModuleLoader(), 43 | withImageModuleLoader(), 44 | ], 45 | }, 46 | plugins: [ 47 | withHTMLPlugin({ 48 | bootstrapConfigInsert: encodeURIComponent( 49 | JSON.stringify({ authType: 'none' }) 50 | ), // template in bootstrap config required, which would normally be provided by the client server module 51 | }), 52 | withNormalModuleReplacementPlugin(), 53 | withMiniCssExtractPlugin({ 54 | hmr: true, // enable hmr as well as standard config 55 | }), 56 | withWebpackBundleAnalyzerPlugin({ 57 | analyzerMode: 'static', 58 | reportFilename: `${BUNDLE_ANALYSER_DIR}/bundles.html`, // when in dev mode, produce a static html file 59 | }), 60 | ], 61 | resolve: { 62 | plugins: [withTsconfigPathsPlugin({ configFile: 'client/tsconfig.json' })], 63 | }, 64 | devServer: { 65 | contentBase: [BUILD_DIR, BUNDLE_ANALYSER_DIR], // static content from the build directory, but also the output of the WebpackBundleAnalyzer configured above (access via /bundles.html) 66 | watchContentBase: true, 67 | compress: true, 68 | inline: true, 69 | hot: true, 70 | https: devEnvToUseTls, 71 | host: webpackDevServer.hostname, 72 | port: webpackDevServer.port, 73 | proxy: [ 74 | { 75 | context: ['**'], 76 | target: `http${devEnvToUseTls ? 's' : ''}://${devServer.hostname}:${ 77 | devServer.port 78 | }`, 79 | secure: false, 80 | }, 81 | ], 82 | overlay: { 83 | warnings: false, 84 | errors: true, 85 | }, 86 | }, 87 | }; 88 | 89 | module.exports = returnBasicConfigMergedWith(devSpecificConfig); 90 | -------------------------------------------------------------------------------- /build/webpack.server.prod.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | // server prod specific plugins and webpack configuration 7 | const TerserPlugin = require('terser-webpack-plugin'); 8 | const nodeExternals = require('webpack-node-externals'); 9 | 10 | const { 11 | returnBasicConfigMergedWith, 12 | plugins, 13 | moduleLoaders, 14 | CONSTANTS, 15 | } = require('./webpack.common.js'); 16 | const { HEADER_TEXT } = require('../utils/tooling/constants.js'); 17 | const { withTypeScriptModuleLoader } = moduleLoaders; 18 | const { withWebpackBundleAnalyzerPlugin, withTsconfigPathsPlugin } = plugins; 19 | 20 | const { 21 | PRODUCTION, 22 | SERVER_BUILD_DIR, 23 | SERVER_DIR, 24 | BUNDLE_ANALYSER_DIR, 25 | } = CONSTANTS; 26 | 27 | const prodSpecificConfig = { 28 | entry: `${SERVER_DIR}/main.ts`, 29 | mode: PRODUCTION, 30 | target: 'node', // build for node 31 | externals: [nodeExternals()], // ignore node_modules - production dependencies install as a part of docker build 32 | output: { 33 | path: SERVER_BUILD_DIR, 34 | filename: '[name].js', 35 | }, 36 | module: { 37 | rules: [withTypeScriptModuleLoader('../server/tsconfig.json')], 38 | }, 39 | optimization: { 40 | minimize: true, 41 | minimizer: [ 42 | new TerserPlugin({ 43 | terserOptions: { 44 | output: { 45 | comments: false, // remove all comments 46 | preamble: HEADER_TEXT, // add strimzi licence to built JS 47 | }, 48 | keep_classnames: true, 49 | keep_fnames: true, 50 | }, 51 | }), 52 | ], 53 | splitChunks: { 54 | chunks: 'all', 55 | name: false, 56 | }, 57 | }, 58 | plugins: [ 59 | // include default bundle analysis 60 | withWebpackBundleAnalyzerPlugin({ 61 | reportFilename: `${BUNDLE_ANALYSER_DIR}/server-report.json`, 62 | }), 63 | ], 64 | resolve: { 65 | plugins: [withTsconfigPathsPlugin({ configFile: 'server/tsconfig.json' })], 66 | }, 67 | }; 68 | 69 | module.exports = returnBasicConfigMergedWith(prodSpecificConfig); 70 | -------------------------------------------------------------------------------- /client/Bootstrap/GraphQLClient/GraphQLClient.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { ApolloClient, HttpLink, split, InMemoryCache } from '@apollo/client'; 7 | 8 | const apiLink = new HttpLink({ uri: '/api', fetch }); 9 | 10 | const configLink = new HttpLink({ uri: '/config', fetch }); 11 | 12 | const splitLink = split( 13 | (operation) => operation.getContext().purpose === 'config', 14 | configLink, 15 | apiLink 16 | ); 17 | 18 | const apolloClient = new ApolloClient({ 19 | link: splitLink, 20 | cache: new InMemoryCache(), 21 | }); 22 | 23 | export { apolloClient }; 24 | -------------------------------------------------------------------------------- /client/Bootstrap/GraphQLClient/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | /* istanbul ignore file */ 7 | 8 | export { apolloClient } from './GraphQLClient'; 9 | -------------------------------------------------------------------------------- /client/Bootstrap/Navigation/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | -------------------------------------------------------------------------------- /client/Bootstrap/Navigation/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './useRouteConfig/useRouteConfig.types'; 6 | -------------------------------------------------------------------------------- /client/Bootstrap/Navigation/useRouteConfig/README.md: -------------------------------------------------------------------------------- 1 | # useRouteConfig 2 | 3 | This hook is used to convert a page configuration into a usuable React-router configuration. 4 | 5 | [View the navigation docs for full details on what it does](/client/Bootstrap/Navigation/README.md#route-configuration-processing). 6 | 7 | ## Usage 8 | 9 | ```ts 10 | 11 | import { useRouteConfig } from 'Navigation'; 12 | import { RouterConfig, PageConfig } from 'Navigation/types'; 13 | 14 | const myComponent = () => { 15 | const pageConfig: PageConfig = {...} 16 | const routerConfig: RouterConfig = useRouteConfig(pageConfig); 17 | } 18 | ``` 19 | -------------------------------------------------------------------------------- /client/Bootstrap/Navigation/useRouteConfig/useRouteConfig.assets.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import React, { FunctionComponent } from 'react'; 6 | 7 | const generateSimplePage = (text: string): FunctionComponent => { 8 | const page = () => { 9 | return
{text}
; 10 | }; 11 | 12 | return page; 13 | }; 14 | 15 | export { generateSimplePage }; 16 | -------------------------------------------------------------------------------- /client/Bootstrap/README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap 2 | 3 | This directory contains all code required to bootstrap the UI. This will include the build entry point `index.ts` ([for further details, see build documentation](../../docs/Build.md)), `index.html` template which will ultimately be returned the to the user when the access the UI, as well as all the React components that set up initial UI state and represent UI level behaviours, such as Navigation or user management. 4 | 5 | While it's own directory, the code in this directory should be tested and developed in the same manner as a [`Panel`](../Panels/README.md) component, but additionally make use of user story end to end tests, which drive the whole UI to achieve the stated user goals. 6 | 7 | ## Test approach 8 | 9 | The code in this directory should only be tested through End-to-End tests. 10 | This is because these files do not contribute unit-level behaviour but have 11 | an impact on the entire UI. This should help speed up development of bootstrap 12 | code and unit testing at this level can be fiddly. See 13 | [End-to-End testing](../../docs/Test.md#end-to-end-testing). 14 | 15 | ## Expected files 16 | 17 | For a given Bootstrap component `Navigation`, the expected files are as follows: 18 | 19 | ``` 20 | Bootstrap/ 21 | index.ts (*) 22 | types.ts 23 | index.html 24 | Navigation/ 25 | index.ts (**) 26 | README.md 27 | View.ts 28 | Model.ts 29 | Styling.scss 30 | Navigation.types.ts 31 | ``` 32 | 33 | Where: 34 | 35 | - index.ts (\*) acts as the build (and thus client) entry point 36 | - types.ts acts as barrel file for types, exporting all public types for each bootstrap component 37 | - index.html is the HTML template file which is used to 38 | - index.ts (\*\*) acts as a barrel file, exporting all public elements of this component 39 | - README.md is the readme for this component, detailing design choices and usage 40 | - View.ts is the view logic for this component 41 | - Model.ts (_optional_) is the model (business) logic for this component 42 | - Styling.scss (_optional_) is the styling for this component 43 | - Navigation.types.ts contains all types for this component that can be used externally 44 | 45 | ## Components 46 | 47 | - [`Navigation`](./Navigation/README.md) - responsible for all Navigation logic (both visual and logical) across the whole UI. 48 | -------------------------------------------------------------------------------- /client/Bootstrap/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /client/Bootstrap/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import ReactDOM from 'react-dom'; 6 | import React from 'react'; 7 | import { init } from 'i18n'; 8 | import { ApolloProvider } from '@apollo/client'; 9 | 10 | import { apolloClient } from 'Bootstrap/GraphQLClient'; 11 | import { ConfigFeatureFlagProvider, FeatureFlag } from 'Contexts'; 12 | import { Home } from 'Panels'; 13 | import { LoggingProvider } from 'Contexts'; 14 | 15 | init(); //Bootstrap i18next support 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | , 26 | document.getElementById('root') 27 | ); 28 | -------------------------------------------------------------------------------- /client/Contexts/ConfigFeatureFlag/ConfigFeatureFlag.assets.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | // deliberately not using the barrel as causes a circular dependency 6 | import { 7 | generateMockDataResponseForGQLRequest, 8 | generateMockErrorResponseForGQLRequest, 9 | } from 'utils/test/withApollo/withApollo.util'; 10 | import { 11 | ConfigFeatureFlagType, 12 | ApolloQueryResponseType, 13 | } from './ConfigFeatureFlag.types'; 14 | import { GET_CONFIG } from 'Queries/Config'; 15 | 16 | export const defaultClientConfig: ApolloQueryResponseType = { 17 | client: {}, 18 | featureFlags: {}, 19 | }; 20 | 21 | /** default values/schema for this context */ 22 | export const defaultConfigFeatureFlagValue: ConfigFeatureFlagType = { 23 | ...defaultClientConfig, 24 | bootstrapConfig: { 25 | auth: 'none', 26 | }, 27 | loading: false, 28 | error: false, 29 | isComplete: true, 30 | rawResponse: {}, 31 | // ignore for code coverage - exists to provide the shape defined by ConfigFeatureFlagType 32 | triggerRefetch: /* istanbul ignore next */ () => { 33 | // NO-OP - placeholder 34 | }, 35 | }; 36 | 37 | const mockClientResponse = { 38 | about: { 39 | version: '1.2.3', 40 | }, 41 | }; 42 | const mockFeatureFlagResponse = { 43 | client: { 44 | Home: { 45 | showVersion: true, 46 | }, 47 | Pages: { 48 | PlaceholderHome: true, 49 | }, 50 | }, 51 | }; 52 | 53 | const mockError = new Error('Example error case'); 54 | 55 | export const mockData = { 56 | mockClientResponse, 57 | mockFeatureFlagResponse, 58 | mockError, 59 | }; 60 | 61 | const additionalRequestArgs = { 62 | variables: {}, 63 | context: { 64 | purpose: 'config', 65 | }, 66 | }; 67 | 68 | const noOpRequest = generateMockDataResponseForGQLRequest( 69 | GET_CONFIG, 70 | {}, 71 | additionalRequestArgs 72 | ); 73 | const successRequest = generateMockDataResponseForGQLRequest( 74 | GET_CONFIG, 75 | { 76 | client: mockClientResponse, 77 | featureFlags: mockFeatureFlagResponse, 78 | }, 79 | additionalRequestArgs 80 | ); 81 | const errorRequest = generateMockErrorResponseForGQLRequest( 82 | GET_CONFIG, 83 | mockError, 84 | additionalRequestArgs 85 | ); 86 | 87 | export const mockRequests = { 88 | noOpRequest, 89 | successRequest, 90 | errorRequest, 91 | }; 92 | -------------------------------------------------------------------------------- /client/Contexts/ConfigFeatureFlag/ConfigFeatureFlag.types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { ApolloError, QueryResult } from '@apollo/client'; 6 | import { 7 | exposedClientType, 8 | exposedFeatureFlagsType, 9 | Literal, 10 | } from 'ui-config/types'; 11 | 12 | /** the response (`data`) value from the graphql config query (client/Queries/Config/index.ts) */ 13 | export type ApolloQueryResponseType = { 14 | /** all client values returned */ 15 | client: exposedClientType; 16 | /** all feature flag values returned */ 17 | featureFlags: exposedFeatureFlagsType; 18 | }; 19 | /** the shape of the `value` returned by the `ConfigFeatureFlag` provider */ 20 | export type ConfigFeatureFlagType = { 21 | /** retrieved client configuration values */ 22 | client: exposedClientType; 23 | /** retrieved feature flag values */ 24 | featureFlags: exposedFeatureFlagsType; 25 | /** core bootstrap config items - deliberately restricted to an object of `Literal` pairs */ 26 | bootstrapConfig: Record; 27 | /** is the request for config in progress? `true` if so, else `false` */ 28 | loading: boolean; 29 | /** did the request error? `true` if so, else `false` */ 30 | error: boolean; 31 | /** has the request completed (ie it is not in either loading or error state)? `true` if so, else `false` */ 32 | isComplete: boolean; 33 | /** the raw response from apollo - either the error object, the data object, or an empty object (before a load or error occurs) */ 34 | rawResponse: ApolloError | QueryResult | Record; 35 | /** function to trigger a new fetch of configuration. Expected to be used in error scenarios */ 36 | triggerRefetch: () => void; 37 | }; 38 | -------------------------------------------------------------------------------- /client/Contexts/ConfigFeatureFlag/Context.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import React, { createContext, FunctionComponent } from 'react'; 6 | import set from 'lodash.set'; 7 | import merge from 'lodash.merge'; 8 | import { useQuery } from '@apollo/client'; 9 | import { GET_CONFIG } from 'Queries/Config'; 10 | import { sanitiseUrlParams, getLocation } from 'Utils'; 11 | import { 12 | defaultClientConfig, 13 | defaultConfigFeatureFlagValue, 14 | } from './ConfigFeatureFlag.assets'; 15 | 16 | import { 17 | ApolloQueryResponseType, 18 | ConfigFeatureFlagType, 19 | } from './ConfigFeatureFlag.types'; 20 | /** ConfigFeatureFlag context - exported solely for use in test and the `useConfigFeatureFlag` hook. Please use the hook or the exported provider/consumer components to access values at runtime */ 21 | const ConfigFeatureFlag = createContext(defaultConfigFeatureFlagValue); 22 | /** ConfigFeatureFlagConsumer - consumer component of the ConfigFeatureFlag context */ 23 | const ConfigFeatureFlagConsumer = ConfigFeatureFlag.Consumer; 24 | /** ConfigFeatureFlagProvider - component which sets up and provides a value to the ConfigFeatureFlag context. Should only be used in the Bootstrap `index.ts` entry point file */ 25 | const ConfigFeatureFlagProvider: FunctionComponent = ({ 26 | children, 27 | ...others 28 | }) => { 29 | // make the query for the config 30 | const { loading, error, data, refetch } = useQuery(GET_CONFIG, { 31 | context: { 32 | purpose: 'config', 33 | }, 34 | }); 35 | 36 | // check to see if we have the raw bootstrap config element 37 | const rawBootstrapConfigElement = document.querySelector( 38 | 'meta[name="bootstrapConfigs"]' 39 | ); 40 | 41 | let bootstrapConfig; 42 | if (rawBootstrapConfigElement !== null) { 43 | const configElementContent = rawBootstrapConfigElement.getAttribute( 44 | 'content' 45 | ); 46 | bootstrapConfig = 47 | configElementContent !== null 48 | ? JSON.parse(decodeURIComponent(configElementContent)) 49 | : defaultConfigFeatureFlagValue.bootstrapConfig; 50 | } else { 51 | bootstrapConfig = defaultConfigFeatureFlagValue.bootstrapConfig; 52 | } 53 | 54 | const { client, featureFlags }: ApolloQueryResponseType = 55 | data || defaultClientConfig; 56 | 57 | // check to see/merge any feature flag state with any defined in browser 58 | const sanitisedParams = sanitiseUrlParams(getLocation().search); 59 | const featureFlagsFromUrl = sanitisedParams.ff 60 | ? sanitisedParams.ff 61 | .split(',') 62 | .map((encodedKeyValuePair) => encodedKeyValuePair.split('=')) 63 | .reduce((acc, [ffKey, value]) => { 64 | return set(acc, ffKey, value === 'true'); 65 | }, {}) 66 | : {}; 67 | const featureFlagsWithUrlFlags = merge( 68 | merge({}, featureFlags), 69 | featureFlagsFromUrl 70 | ); 71 | 72 | const value: ConfigFeatureFlagType = { 73 | client, 74 | featureFlags: featureFlagsWithUrlFlags, 75 | bootstrapConfig, 76 | loading, 77 | error: error ? true : false, 78 | isComplete: !loading && !error, 79 | triggerRefetch: () => { 80 | refetch(); 81 | }, 82 | rawResponse: error ? error : data ? data : {}, 83 | }; 84 | 85 | return ( 86 | 87 | {children} 88 | 89 | ); 90 | }; 91 | 92 | export { 93 | ConfigFeatureFlagProvider, 94 | ConfigFeatureFlagConsumer, 95 | ConfigFeatureFlag, 96 | }; 97 | -------------------------------------------------------------------------------- /client/Contexts/ConfigFeatureFlag/FeatureFlag.spec.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { RenderResult } from '@testing-library/react'; 6 | import React from 'react'; 7 | import { renderWithCustomConfigFeatureFlagContext } from 'utils/test'; 8 | import { defaultClientConfig } from './ConfigFeatureFlag.assets'; 9 | import { FeatureFlag } from './FeatureFlag.view'; 10 | 11 | describe('FeatureFlag component', () => { 12 | const testConfigFeatureFlagState = { 13 | ...defaultClientConfig, 14 | featureFlags: { 15 | flag: { 16 | settingOne: true, 17 | settingTwo: false, 18 | }, 19 | }, 20 | }; 21 | 22 | const renderForTestFFComponentWithFlag: (flag: string) => RenderResult = ( 23 | flag 24 | ) => 25 | renderWithCustomConfigFeatureFlagContext( 26 | testConfigFeatureFlagState, 27 | My Feature 28 | ); 29 | 30 | it('renders a feature when the flag is enabled', () => { 31 | const { queryByText } = renderForTestFFComponentWithFlag('flag.settingOne'); 32 | expect(queryByText('My Feature')).toBeInTheDocument(); 33 | }); 34 | 35 | it('does not render a feature when the flag is disabled', () => { 36 | const { queryByText } = renderForTestFFComponentWithFlag('flag.settingTwo'); 37 | expect(queryByText('My Feature')).not.toBeInTheDocument(); 38 | }); 39 | 40 | it('does not render a feature when the flag is not found', () => { 41 | const { queryByText } = renderForTestFFComponentWithFlag('doesnt.exist'); 42 | expect(queryByText('My Feature')).not.toBeInTheDocument(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /client/Contexts/ConfigFeatureFlag/FeatureFlag.view.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import React, { FunctionComponent } from 'react'; 6 | import get from 'lodash.get'; 7 | import { useConfigFeatureFlag } from 'Hooks'; 8 | 9 | type featureFlagPropsType = { 10 | /** the json path of the flag - eg `client.Home.showVersion` */ 11 | flag: string; 12 | }; 13 | 14 | export const FeatureFlag: FunctionComponent = ({ 15 | children, 16 | flag, 17 | ...others 18 | }) => { 19 | const { featureFlags } = useConfigFeatureFlag(); 20 | 21 | const featureFlagValue = get(featureFlags, flag, false); 22 | 23 | return ( 24 |
25 | {featureFlagValue ? children : null} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /client/Contexts/ConfigFeatureFlag/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './Context'; 6 | export * from './ConfigFeatureFlag.assets'; 7 | export * from './FeatureFlag.view'; 8 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/ExpectationTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import {IntrospectionField} from "graphql"; 6 | 7 | /** 8 | * An expectation is defined per entity we expect e.g. a Topic 9 | */ 10 | export interface Expectation { 11 | /** 12 | * The type is the GraphQL type for this expectation 13 | */ 14 | type: string; 15 | /** 16 | * The fields that we expect this entity to have 17 | */ 18 | fields?: { 19 | /** 20 | * The field is defined as a GraphQL type 21 | */ 22 | [key: string]: string; 23 | }; 24 | /** 25 | * The operations that we expect this entity to have 26 | */ 27 | operations?: { 28 | /** 29 | * The operation is validated using a callback 30 | */ 31 | [key: string]: OperationCallback; 32 | }; 33 | /** 34 | * The subscriptions that we expect this entity to have 35 | */ 36 | subscriptions?: { 37 | /** 38 | * The subscription is validated using a callback 39 | */ 40 | [key: string]: SubscriptionCallback; 41 | }; 42 | } 43 | 44 | /** 45 | * A collection of expected entities 46 | */ 47 | export interface Expectations { 48 | [key: string]: Expectation; 49 | } 50 | 51 | /** 52 | * An entity expresses whether an expectation has been met or not 53 | */ 54 | export interface Entity { 55 | /** 56 | * The type is the GraphQL type for this expectation 57 | */ 58 | type: string; 59 | /** 60 | * The fields that we expect this entity to have 61 | */ 62 | fields: { 63 | /** 64 | * Whether the field met the expectation 65 | */ 66 | [key: string]: boolean; 67 | }; 68 | /** 69 | * The operations that we expect this entity to have 70 | */ 71 | operations: { 72 | /** 73 | * Whether the operation met the expectation 74 | */ 75 | [key: string]: boolean; 76 | }; 77 | /** 78 | * The subscriptions that we expect this entity to have 79 | */ 80 | subscriptions: { 81 | /** 82 | * Whether the subscription met the expectation 83 | */ 84 | [key: string]: boolean; 85 | }; 86 | } 87 | 88 | /** 89 | * The callback properties for an operation expectation 90 | */ 91 | export interface OperationCallbackProps { 92 | /** 93 | * All the queries defined 94 | */ 95 | queries: { [key: string]: IntrospectionField }; 96 | /** 97 | * All the mutations defined 98 | */ 99 | mutations: { [key: string]: IntrospectionField }; 100 | } 101 | 102 | /** 103 | * The callback properties for a subscription expectation 104 | */ 105 | export interface SubscriptionCallbackProps { 106 | /** 107 | * All the subscriptions defined 108 | */ 109 | subscriptions: { [key: string]: IntrospectionField }; 110 | } 111 | 112 | /** 113 | * The callback for an operation expectation 114 | */ 115 | export interface OperationCallback { 116 | (props: OperationCallbackProps): boolean; 117 | } 118 | 119 | /** 120 | * The callback for a subscription expectation 121 | */ 122 | export interface SubscriptionCallback { 123 | (props: SubscriptionCallbackProps): boolean; 124 | } 125 | 126 | /** 127 | * A collection of entities 128 | */ 129 | export interface Entities { 130 | [key: string]: Entity; 131 | } 132 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/Introspection.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import expectationsBasic from "./mock/expectations-basic"; 6 | import expectationsMissingMutation from "./mock/expectations-missing-mutation"; 7 | import introspectionBasic from "./mock/mock-introspection"; 8 | import expectionsEmptyTopic from './mock/expectations-empty-topic'; 9 | import expectationsMissingType from "./mock/expectations-missing-type" 10 | import introspectionMissingMutation from "./mock/mock-introspection-missing-mutation-type" 11 | import introspectionUnsupportedFieldType from "./mock/mock-introspection-unsupported-field-type"; 12 | import introspectionUnindexable from "./mock/mock-introspection-unindexable"; 13 | import introspectionMissingMutationBlock from "./mock/mock-introspection-missing-mutation-block"; 14 | import introspectionWrongTypeOnTopic from "./mock/mock-introspection-wrong-type-on-topic"; 15 | import introspectionWrongTypeOnMutationBlock from "./mock/mock-introspection-wrong-type-on-mutation-block"; 16 | import {introspect} from "./Introspection"; 17 | import {entitiesBasic} from "./mock/mock-entities"; 18 | 19 | describe("Basic Introspection", () => { 20 | it("should match", () => 21 | { 22 | const introspected = introspect(expectationsBasic, introspectionBasic); 23 | expect(introspected).toEqual(entitiesBasic); 24 | }); 25 | }); 26 | 27 | describe("Missing Type", () => { 28 | it("should error", () => 29 | { 30 | expect(() => {introspect(expectationsMissingType, introspectionBasic)}).toThrowError("Unable to find a type for Foo"); 31 | 32 | }); 33 | }); 34 | 35 | describe("Missing Mutation Type", () => { 36 | it("should error", () => 37 | { 38 | expect(() => {introspect(expectationsMissingMutation, introspectionMissingMutation)}).toThrowError("mutations is empty"); 39 | 40 | }); 41 | }); 42 | 43 | describe("Unsupported Field Type", () => { 44 | it("should error", () => 45 | { 46 | expect(() => {introspect(expectationsBasic, introspectionUnsupportedFieldType)}).toThrowError("Unsupported graphql kind UNION for String"); 47 | 48 | }); 49 | }); 50 | 51 | describe("Unindexable types", () => { 52 | it("should error", () => 53 | { 54 | expect(() => {introspect(expectationsBasic, introspectionUnindexable)}).toThrowError("key identified by name must be of type string"); 55 | 56 | }); 57 | }); 58 | 59 | describe("Missing mutation block", () => { 60 | it("should error", () => 61 | { 62 | expect(() => {introspect(expectationsBasic, introspectionMissingMutationBlock)}).toThrowError("Unable to find a type for Mutation"); 63 | 64 | }); 65 | }); 66 | 67 | describe("Wrong type on mutation block", () => { 68 | it("should error", () => 69 | { 70 | expect(() => {introspect(expectationsMissingMutation, introspectionWrongTypeOnMutationBlock)}).toThrowError("mutations is empty"); 71 | 72 | }); 73 | }); 74 | 75 | describe("Non object type", () => { 76 | it("should error", () => 77 | { 78 | const introspected = introspect(expectionsEmptyTopic, introspectionWrongTypeOnTopic); 79 | expect(introspected); 80 | 81 | }); 82 | }); 83 | 84 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/mock/expectations-basic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { Expectations } from '../ExpectationTypes'; 7 | 8 | export default { 9 | Other: { 10 | type: 'Other', 11 | fields: { 12 | names: '[String]', 13 | size: 'Int', // TODO how do we handle args? 14 | roughSize: 'TShirtSize', 15 | requiredRoughSize: 'TShirtSize!', 16 | possibleRoughSizes: '[TShirtSize]', 17 | requiredNames: '[String]!', 18 | nonNullNames: '[String!]', 19 | nonNullRequiredNames: '[String!]!', 20 | topics: '[Topic]', 21 | }, 22 | }, 23 | Topic: { 24 | type: 'Topic', // the GraphQL type name 25 | fields: { 26 | name: 'String!', // the expected GraphQL type - could be string or object - which is checked if is available and thus query-able 27 | partitions: 'Float', 28 | replicas: 'Int', 29 | }, 30 | operations: { 31 | create: ({ mutations }) => { 32 | return mutations['createTopic'] !== undefined; 33 | }, // function that checks for a named mutation which provides the ability to create a topic for example 34 | update: ({ mutations }) => { 35 | return mutations['updateTopic'] !== undefined; 36 | }, 37 | delete: ({ mutations }) => { 38 | return mutations['deleteTopic'] !== undefined; 39 | }, 40 | findByName: ({ queries }) => { 41 | return ( 42 | queries['topicsByName'] !== undefined && 43 | queries['topicsByName'].args.findIndex((v) => v.name === 'name') > -1 44 | ); 45 | }, 46 | }, 47 | subscriptions: { 48 | topicsUpdate: ({ subscriptions }) => { 49 | return subscriptions['topicAdded'] && 50 | subscriptions['topicAdded'].type.kind === 'OBJECT' && 51 | subscriptions['topicAdded'].type.name === 'Topic'; 52 | }, // function that checks for a named subscription which relates to topics 53 | }, 54 | }, 55 | } as Expectations; 56 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/mock/expectations-empty-topic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { Expectations } from '../ExpectationTypes'; 7 | 8 | export default { 9 | Topic: { 10 | type: 'Topic', // the GraphQL type name 11 | fields: {} 12 | }, 13 | } as Expectations; 14 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/mock/expectations-missing-mutation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { Expectations } from '../ExpectationTypes'; 7 | 8 | export default { 9 | Topic: { 10 | type: 'Topic', // the GraphQL type name 11 | operations: { 12 | create: ({ mutations }) => { 13 | if (Object.keys(mutations).length === 0) { 14 | throw new Error("mutations is empty") 15 | } 16 | return true; 17 | }, 18 | } 19 | }, 20 | } as Expectations; 21 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/mock/expectations-missing-type.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { Expectations } from '../ExpectationTypes'; 7 | 8 | export default { 9 | Foo: { 10 | type: 'Foo' 11 | } 12 | } as Expectations; 13 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/mock/mock-entities-empty-topic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export const entitiesBasic = { 6 | Other: { 7 | fields: { 8 | names: true, 9 | size: true, 10 | roughSize: true, 11 | requiredRoughSize: true, 12 | possibleRoughSizes: true, 13 | requiredNames: true, 14 | nonNullNames: true, 15 | nonNullRequiredNames: true, 16 | topics: true, 17 | }, 18 | operations: {}, 19 | subscriptions: {}, 20 | type: 'Other', 21 | }, 22 | Topic: { 23 | fields: { }, 24 | operations: { create: true, update: true, delete: false, findByName: true }, 25 | subscriptions: { topicsUpdate: true }, 26 | type: 'Topic', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /client/Contexts/Introspect/mock/mock-entities.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export const entitiesBasic = { 6 | Other: { 7 | fields: { 8 | names: true, 9 | size: true, 10 | roughSize: true, 11 | requiredRoughSize: true, 12 | possibleRoughSizes: true, 13 | requiredNames: true, 14 | nonNullNames: true, 15 | nonNullRequiredNames: true, 16 | topics: true, 17 | }, 18 | operations: {}, 19 | subscriptions: {}, 20 | type: 'Other', 21 | }, 22 | Topic: { 23 | fields: { name: true, partitions: false, replicas: true }, 24 | operations: { create: true, update: true, delete: false, findByName: true }, 25 | subscriptions: { topicsUpdate: true }, 26 | type: 'Topic', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /client/Contexts/Logging/Context.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import React, { createContext, FunctionComponent } from 'react'; 6 | import { LoggingStateType } from './Logging.types'; 7 | 8 | const initialState: LoggingStateType = { 9 | websocket: null, 10 | messageBuffer: [], 11 | }; 12 | 13 | const LoggingContext = createContext | null>(null); 16 | 17 | const LoggingProvider: FunctionComponent = ({ children, ...others }) => { 18 | // Use a ref to store the state as it needs to be immutable 19 | const loggingRef = React.useRef(initialState); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export { LoggingProvider, LoggingContext }; 29 | -------------------------------------------------------------------------------- /client/Contexts/Logging/Logging.types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export type LoggingStateType = { 6 | websocket: WebSocket | null; 7 | messageBuffer: loggerMessage[]; 8 | }; 9 | 10 | export interface loggerMessage { 11 | clientTime: number; 12 | clientID: string; 13 | clientLevel: LogLevel; 14 | componentName: string; 15 | msg: string; 16 | } 17 | 18 | export enum LogLevel { 19 | fatal = 'fatal', 20 | error = 'error', 21 | warn = 'warn', 22 | info = 'info', 23 | debug = 'debug', 24 | trace = 'trace', 25 | } 26 | 27 | export type LogLevelType = keyof typeof LogLevel; 28 | -------------------------------------------------------------------------------- /client/Contexts/Logging/README.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | This context will store the logging WebSocket client connection and the buffer of logging messages used in the (useLogger)[../../Hooks/useLogger] hook. This allows the useLogger hook to use a single WebSocket connection for all logging calls. 4 | -------------------------------------------------------------------------------- /client/Contexts/Logging/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './Logging.types'; 6 | export * from './Context'; 7 | -------------------------------------------------------------------------------- /client/Contexts/README.md: -------------------------------------------------------------------------------- 1 | # Contexts 2 | 3 | This UI makes use of [React Context](https://reactjs.org/docs/context.html) for top level state management. This allows for data to be stored and accessed by any component without needing to prop drill values to that component. In addition, depending on use case, contexts can be used as a component or as a React Hook, allowing for integration of state where ever and however it is required. This directory contains all the custom React Context implementations used across this UI. 4 | 5 | It is expected that when used in production Context Provider(s) will be used by the code contained in the [`Bootstrap`](../Bootstrap/README.md) directory. Consumers of the state could be either other [`Bootstrap`](../Bootstrap/README.md) components, or [`Panel`](../Panels/README.md) or [`Group`](../Groups/README.md) components. 6 | 7 | ## Test approach 8 | 9 | Elements should be tested in a functional manor. See [Test Driven Development](../../docs/Test.md#style-of-test). 10 | 11 | ## Expected files 12 | 13 | For a given new Context `FeatureFlagContext`, the expected files are as follows: 14 | 15 | ``` 16 | Contexts/ 17 | index.ts 18 | types.ts 19 | FeatureFlagContext/ 20 | README.md 21 | FeatureFlagContext.ts 22 | FeatureFlagContext.assets.ts 23 | FeatureFlagContext.spec.ts 24 | FeatureFlagContext.types.ts 25 | ``` 26 | 27 | Where: 28 | 29 | - index.ts acts as a barrel file, exporting all public elements of this context/the contexts contained in the Context directory 30 | - types.ts acts as a barrel file, exporting all the public types of each context 31 | - README.md is the readme for this Context, detailing design choices and usage 32 | - FeatureFlagContext.ts is the implementation of this context 33 | - FeatureFlagContext.spec.ts are the tests for this context 34 | - FeatureFlagContext.assets.ts are the test assets for this context 35 | - FeatureFlagContext.types.ts are the types for this context 36 | 37 | ## Available contexts 38 | 39 | - [`Introspect`](./Introspect/README.md) - responsible for fetching, parsing and providing the UI with the supported capabilities of the backend GraphQL server 40 | - [`ConfigFeatureFlag`](./ConfigFeatureFlag/README.md) - responsible for fetching and providing the current configuration and feature flag state, retrieved from the UI Server [config module](../../server/config/README.md) 41 | -------------------------------------------------------------------------------- /client/Contexts/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './ConfigFeatureFlag'; 6 | export * from './Introspect/ExpectationTypes'; 7 | export * from './Logging'; 8 | -------------------------------------------------------------------------------- /client/Contexts/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './ConfigFeatureFlag/ConfigFeatureFlag.types'; 6 | -------------------------------------------------------------------------------- /client/Elements/README.md: -------------------------------------------------------------------------------- 1 | # Elements 2 | 3 | `Element` components are presentational React components: they take zero to many properties, and render content based on those properties. They should not contain or perform any business logic themselves, such as accessing state or making requests for data. They could however call callbacks provided to them to trigger these effects. By staying presentational, they thus become highly composable and reusable. 4 | 5 | ## Test approach 6 | 7 | Bootstrap should be tested in a behavioural manor. See [Behavioural Driven Development](../../docs/Test.md#style-of-test). 8 | 9 | ## Expected files 10 | 11 | For a given Element component `Heading`, the expected files are as follows: 12 | 13 | ``` 14 | Elements/ 15 | index.ts 16 | types.ts 17 | Heading/ 18 | index.ts 19 | README.md 20 | View.ts 21 | Styling.scss 22 | Heading.types.ts 23 | Heading.feature 24 | Heading.steps.ts 25 | Heading.assets.ts 26 | ``` 27 | 28 | Where: 29 | 30 | - index.ts acts as a barrel file, exporting all public elements of this component/the components in the Elements directory 31 | - types.ts acts as a barrel file, exporting all the public types of each element 32 | - README.md is the readme for this component, detailing design choices and usage 33 | - View.ts is the view logic for this component 34 | - Styling.scss (_optional_) is the styling for this component 35 | - Heading.types.ts are all the types for this component 36 | - heading.feature is the feature definition to test this component 37 | - Heading.steps.ts are the steps for the feature file 38 | - Heading.assets.ts are the test assets for this component 39 | 40 | ## Components 41 | 42 | Components to be added here on implementation, with summary of purpose/usage and a link to it's README. 43 | -------------------------------------------------------------------------------- /client/Groups/README.md: -------------------------------------------------------------------------------- 1 | # Groups 2 | 3 | `Group` components are one or more `Element` components combined and composed together to provide a larger piece of UI. They can own an manage their own state and business logic via a Model if required, and use then use that state (via properties) in the child `Element` components used in this `Group`. By doing this, the `Group` component can act as a layer of abstraction between `Element` components which build up the `Group`, and the `Panel` (and it's global state/data fetching logic for example) the `Group` is used in. This keeps the `Element` and `Group` components as composable as possible, while keeping `Panel` logic together. 4 | 5 | ## Test approach 6 | 7 | Bootstrap should be tested in a behavioural manor. See [Behavioural Driven Development](../../docs/Test.md#style-of-test). 8 | 9 | ## Expected files 10 | 11 | For a given Group component `HeadingWithToggle`, the expected files are as follows: 12 | 13 | ``` 14 | Groups/ 15 | index.ts 16 | types.ts 17 | HeadingWithToggle/ 18 | index.ts 19 | README.md 20 | View.ts 21 | Model.ts 22 | Styling.scss 23 | HeadingWithToggle.feature 24 | HeadingWithToggle.steps.ts 25 | HeadingWithToggle.assets.ts 26 | HeadingWithtoggle.types.ts 27 | ``` 28 | 29 | Where: 30 | 31 | - index.ts acts as a barrel file, exporting all public elements of this component/the components in the Groups directory 32 | - types.ts acts as a barrel file, exporting all the public types of each context 33 | - README.md is the readme for this component, detailing design choices and usage 34 | - View.ts is the view logic for this component 35 | - Model.ts (_optional_) is the model (business) logic for this component 36 | - Styling.scss (_optional_) is the styling for this component 37 | - HeadingWithToggle.feature is the feature definitionn file to test this component 38 | - HeadingWithToggle.steps.ts are the steps for the component feature 39 | - HeadingWithToggle.assets.ts are the test assets for this component 40 | - HeadingWithToggle.types.ts are the types for this component 41 | 42 | ## Components 43 | 44 | Components to be added here on implementation, with summary of purpose/usage and a link to it's README. 45 | -------------------------------------------------------------------------------- /client/Hooks/README.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | This directory contains all re-usable custom [React Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) implemented for use across this UI. Unlike Models, these hooks encapsulate re-usable/common logic views or models may want to utilise, such as translation capabilities. 4 | 5 | ## Test approach 6 | 7 | Elements should be tested in a functional manor. See [Test Driven Development](../../docs/Test.md#style-of-test). 8 | 9 | ## Expected files 10 | 11 | For a given Hook `useFoo`, the expected files are as follows: 12 | 13 | ``` 14 | Hooks/ 15 | index.ts 16 | types.ts 17 | useFoo/ 18 | README.md 19 | useFoo.ts 20 | useFoo.spec.ts 21 | useFoo.assets.ts 22 | useFoo.types.ts 23 | ``` 24 | 25 | Where: 26 | 27 | - index.ts acts as a barrel file, exporting the hooks defined in the Hooks directory 28 | - types.ts acts as a barrel file, exporting all the public types of each context 29 | - README.md is the readme for this hook, detailing design choices and usage 30 | - useFoo.ts is the hook implementation 31 | - useFoo.spec.ts are the tests for this hook 32 | - useFoo.assets.ts are the test assets for this hook 33 | - useFoo.types.ts are the types for this hook 34 | 35 | ## Implemented hooks 36 | 37 | - [`useConfigFeatureFlag`](./useConfigFeatureFlag/README.md) - a hook providing consumer access to the `ConfigFeatureFlag` context, containing configuration and feature flag state 38 | - [`useLogger`](./useLogger/README.md) - sends client log messages to the WebSocket listener on the server `/log` endpoint. 39 | -------------------------------------------------------------------------------- /client/Hooks/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './useConfigFeatureFlag/useConfigFeatureFlag'; 6 | export * from './useLogger'; 7 | -------------------------------------------------------------------------------- /client/Hooks/useConfigFeatureFlag/README.md: -------------------------------------------------------------------------------- 1 | # useConfigFeatureFlag 2 | 3 | The `useConfigFeatureFlag` hook provides access to the value provided by the `ConfigFeatureFlag` context. [Further details about the value returned can be found here](../../Contexts/ConfigFeatureFlag/README.md). 4 | -------------------------------------------------------------------------------- /client/Hooks/useConfigFeatureFlag/useConfigFeatureFlag.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { renderHook } from '@testing-library/react-hooks'; 7 | import { useConfigFeatureFlag } from './useConfigFeatureFlag'; 8 | import { defaultConfigFeatureFlagValue } from 'Contexts'; 9 | 10 | describe('`useConfigFeatureFlag` hook', () => { 11 | // The functions/responses/behaviours of the hook are the same as the components, tested in `client/Contexts/ConfigFeatureFlag/ConfigFeatureFlag.spec.tsx`. This test verifies the hook returns the expected state 12 | it('returns the expected context state', () => { 13 | const { result } = renderHook(() => useConfigFeatureFlag()); 14 | const hookValue = result.current; 15 | expect(hookValue).toEqual(defaultConfigFeatureFlagValue); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /client/Hooks/useConfigFeatureFlag/useConfigFeatureFlag.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { useContext } from 'react'; 6 | import { ConfigFeatureFlag } from 'Contexts'; 7 | import { ConfigFeatureFlagType } from 'Contexts/types'; 8 | 9 | /** useConfigFeatureFlag - hook which returns the current value of the ConfigFeatureFlag context */ 10 | export const useConfigFeatureFlag: () => ConfigFeatureFlagType = () => 11 | useContext(ConfigFeatureFlag); 12 | -------------------------------------------------------------------------------- /client/Hooks/useLogger/README.md: -------------------------------------------------------------------------------- 1 | # useLogger 2 | 3 | This hook is responsible for storing and sending client log messages to the WebSocket listener on the server `/log` endpoint. It sets up the WebSocket connection using the URL in the logging context. 4 | 5 | Logging and the websocket connection are enabled when the `LOGGING` query parameter exists in the URL. The value of the `LOGGING` query parameter is a regex that is used to determine which components and code are logged. For example, a URL like `https://localhost:3000?LOGGING=.*` will enable logging for all components, and a URL like `https://localhost:3000?LOGGING=Home|MyDiv` will enable logging for just the `Home` and `MyDiv` components. 6 | 7 | To use this hook, the (`LoggingProvider`)[../../Contexts/Logging] context provider component must be rendered by an ancestor component. The `Logging` context stores the websocket client and the messages buffer used by this hook. 8 | 9 | Usage of the hook must follow the (react hooks rules)[https://reactjs.org/docs/hooks-rules.html], but the logger callback returned by the hook can be used anywhere. For example: 10 | 11 | ``` 12 | const MyDiv = (props) => { 13 | const { debug, trace } = useLogger('MyDiv'); 14 | debug(`Creating MyDiv component: ${{...props}}`); 15 | 16 | React.useEffect(() => { 17 | debug(`One-time interval set-up for MyDiv component`); 18 | setInterval(() => { 19 | trace('MyDiv component interval'); 20 | }, 5000); 21 | }, []); 22 | 23 | return (
); 24 | }; 25 | ``` 26 | 27 | ## API 28 | 29 | ### `useLogger(componentName: string)` 30 | 31 | The useLogger hook takes the name of the component, which appears in the server-side logs as `componentName`, and returns the `LoggerType` object containing the logging callbacks. The hook makes the connection to the the WebSocket listener on the server `/log` endpoint. 32 | 33 | ### `LoggerType` object 34 | 35 | ``` 36 | { 37 | log: (clientLevel: LogLevel, msg: string) => void; 38 | fatal: (msg: string) => void; 39 | error: (msg: string) => void; 40 | warn: (msg: string) => void; 41 | info: (msg: string) => void; 42 | debug: (msg: string) => void; 43 | trace: (msg: string) => void; 44 | } 45 | ``` 46 | 47 | The `LoggerType` object includes a `log` function which has two arguments - the logging level, which can be `'fatal'`, `'error'`, `'warn'`, `'info'`, `'debug'` or `'trace'`, and determines the level at which the message is logged at the server; and a log message. 48 | 49 | The other `LoggerType` functions each call the `log` function with the appropriate log level applied. 50 | -------------------------------------------------------------------------------- /client/Hooks/useLogger/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './Hook'; 6 | -------------------------------------------------------------------------------- /client/Images/README.md: -------------------------------------------------------------------------------- 1 | # Images 2 | 3 | This directory contains all images used across the UI. All images are considered as public assets - ie no user authentication is required to retrieve them. 4 | -------------------------------------------------------------------------------- /client/Images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strimzi/strimzi-ui/962a778df265ac2863a4ad82e55cd9db5841a8fb/client/Images/favicon.ico -------------------------------------------------------------------------------- /client/Images/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | declare module '*.png' { 6 | const content: string; 7 | export default content; 8 | } 9 | 10 | declare module '*.svg' { 11 | const content: string; 12 | export default content; 13 | } 14 | -------------------------------------------------------------------------------- /client/Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strimzi/strimzi-ui/962a778df265ac2863a4ad82e55cd9db5841a8fb/client/Images/logo.png -------------------------------------------------------------------------------- /client/Panels/Error404/Error404.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: Error 404 component 4 | 5 | Scenario: Basic rendering 6 | Given a Error404 component 7 | When it is rendered 8 | Then it should display text -------------------------------------------------------------------------------- /client/Panels/Error404/Error404.steps.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { Given, When, Then, Fusion } from 'jest-cucumber-fusion'; 6 | import { render, RenderResult } from '@testing-library/react'; 7 | import { Error404 } from '.'; 8 | import React, { ReactElement } from 'react'; 9 | 10 | let renderResult: RenderResult; 11 | let component: ReactElement; 12 | 13 | Given('a Error404 component', () => { 14 | component = ; 15 | }); 16 | 17 | When('it is rendered', () => { 18 | renderResult = render(component); 19 | }); 20 | 21 | Then('it should display text', () => { 22 | const { getByText } = renderResult; 23 | expect(getByText('Error 404')).toBeInTheDocument(); 24 | }); 25 | 26 | Fusion('Error404.feature'); 27 | -------------------------------------------------------------------------------- /client/Panels/Error404/Error404.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import React, { FunctionComponent } from 'react'; 6 | 7 | const Error404: FunctionComponent = () => { 8 | return
Error 404
; 9 | }; 10 | 11 | export { Error404 }; 12 | -------------------------------------------------------------------------------- /client/Panels/Error404/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { Error404 } from './Error404'; 6 | -------------------------------------------------------------------------------- /client/Panels/Home/Home.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: Home component 4 | 5 | Scenario: Basic rendering 6 | Given a Home component 7 | When it is rendered 8 | Then it should display the expected text 9 | 10 | Scenario: Basic rendering without version 11 | Given a Home component 12 | When it is rendered with no version 13 | Then it should display the expected text -------------------------------------------------------------------------------- /client/Panels/Home/Home.steps.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { Given, When, Then, Fusion } from 'jest-cucumber-fusion'; 6 | import { RenderResult } from '@testing-library/react'; 7 | import merge from 'lodash.merge'; 8 | import { renderWithCustomConfigFeatureFlagContext } from 'utils/test'; 9 | import { Home } from '.'; 10 | import React, { ReactElement } from 'react'; 11 | 12 | let renderResult: RenderResult; 13 | let component: ReactElement; 14 | let showVersionSet: boolean; 15 | 16 | const coreConfigFromContext = { 17 | client: { about: { version: '34.34.34' } }, 18 | featureFlags: { 19 | client: { 20 | Home: { 21 | showVersion: true, 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | Given('a Home component', () => { 28 | component = ; 29 | }); 30 | 31 | When('it is rendered', () => { 32 | renderResult = renderWithCustomConfigFeatureFlagContext( 33 | coreConfigFromContext, 34 | component 35 | ); 36 | showVersionSet = true; 37 | }); 38 | 39 | When('it is rendered with no version', () => { 40 | renderResult = renderWithCustomConfigFeatureFlagContext( 41 | merge({}, coreConfigFromContext, { 42 | featureFlags: { 43 | client: { 44 | Home: { 45 | showVersion: false, 46 | }, 47 | }, 48 | }, 49 | }), 50 | component 51 | ); 52 | showVersionSet = false; 53 | }); 54 | 55 | Then('it should display the expected text', () => { 56 | const { getByText, queryByText } = renderResult; 57 | expect(getByText('Welcome to the Strimzi UI')).toBeInTheDocument(); 58 | const versionString = `Version: ${coreConfigFromContext.client.about.version}`; 59 | showVersionSet 60 | ? expect(getByText(versionString)).toBeInTheDocument() 61 | : expect(queryByText(versionString)).not.toBeInTheDocument(); 62 | }); 63 | 64 | Fusion('Home.feature'); 65 | -------------------------------------------------------------------------------- /client/Panels/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import React, { FunctionComponent } from 'react'; 6 | import get from 'lodash.get'; 7 | import image from 'Images/logo.png'; 8 | import './style.scss'; 9 | import { useConfigFeatureFlag, useLogger } from 'Hooks'; 10 | 11 | const Home: FunctionComponent = ({ children }) => { 12 | const { client, featureFlags, isComplete } = useConfigFeatureFlag(); 13 | const version = get(client, 'about.version', ''); 14 | // use the feature flag from context - could also use the `FeatureFlag` component - this just shows alternative usage 15 | const showVersion = get(featureFlags, 'client.Home.showVersion', false); 16 | 17 | const { debug } = useLogger('Home'); 18 | debug(`Client version to display: ${version}`); 19 | 20 | return ( 21 |
22 | Strimzi logo 23 |

Welcome to the Strimzi UI

24 | {showVersion && isComplete &&

{`Version: ${version}`}

} 25 | {children} 26 |
27 | ); 28 | }; 29 | 30 | export { Home }; 31 | -------------------------------------------------------------------------------- /client/Panels/Home/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { Home } from './Home'; 6 | -------------------------------------------------------------------------------- /client/Panels/Home/style.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | .home { 7 | align-items: center; 8 | color: #182c46; 9 | display: flex; 10 | flex-direction: column; 11 | font-family: 'Nunito', 'Open Sans', sans-serif; 12 | height: 98vh; 13 | justify-content: center; 14 | } 15 | -------------------------------------------------------------------------------- /client/Panels/README.md: -------------------------------------------------------------------------------- 1 | # Panels 2 | 3 | A `Panel` component represents a top level element of a UI - the primary/significant capability of a page. A `Panel`s responsibility is not only to compose `Element` and `Group` components to implement the required UI, but to own and manage activities such as data fetching, state reduction, integration with the wider UI and so on. For example, a `Panel` would on mount make a request for and own the response for a piece of data, and while this request is happening, swap in and out `Group` and or `Element` components to represent the loading/error/success states. 4 | 5 | ## Test approach 6 | 7 | Bootstrap should be tested in a behavioural manor. See [Behavioural Driven Development](../../docs/Test.md#style-of-test). 8 | 9 | ## Expected files 10 | 11 | For a given Panel component `Topics`, the expected files are as follows: 12 | 13 | ``` 14 | Panels/ 15 | index.ts 16 | types.ts 17 | Topics/ 18 | index.ts 19 | README.md 20 | View.ts 21 | Model.ts 22 | Styling.scss 23 | Topics.feature 24 | Topics.steps.ts 25 | Topics.assets.ts 26 | Topics.types.ts 27 | ``` 28 | 29 | Where: 30 | 31 | - index.ts acts as a barrel file, exporting the public API of this component/component bundle. 32 | - types.ts acts as a barrel file, exporting all the public types of each context 33 | - README.md is the readme for this component, detailing design choices and usage 34 | - View.ts is the view logic for this component 35 | - Model.ts (_optional_) is the model (business) logic for this component 36 | - Styling.scss (_optional_) is the styling for this component 37 | - Topics.feature is the test definition file for this page 38 | - Topics.steps.ts are the steps for the feature definition 39 | - Topics.assets.ts are the test assets for this page 40 | - Topics.types.ts are the types for this page 41 | 42 | ## Components 43 | 44 | Components to be added here on implementation, with summary of purpose/usage and a link to it's README. 45 | -------------------------------------------------------------------------------- /client/Panels/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './Error404'; 6 | export * from './Home'; 7 | -------------------------------------------------------------------------------- /client/Queries/Config/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import gql from 'graphql-tag'; 6 | 7 | export const GET_CONFIG = gql` 8 | query { 9 | client { 10 | about { 11 | version 12 | } 13 | } 14 | featureFlags { 15 | client { 16 | Home { 17 | showVersion 18 | } 19 | Pages { 20 | PlaceholderHome 21 | } 22 | } 23 | } 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /client/Queries/README.md: -------------------------------------------------------------------------------- 1 | # Queries 2 | 3 | This directory is home to all GraphQL queries. These should be imported by the appropriate models and exposed via a hook. 4 | 5 | ## File Structure 6 | 7 | - Queries 8 | - query set (e.g. topics) 9 | - index.ts - containing all gql queries for that set 10 | 11 | ## Examples 12 | 13 | ### API 14 | 15 | ```typescript 16 | // topics/index.ts 17 | 18 | import gql from 'graphql-tag'; 19 | 20 | export const GET_TOPICS = gql` 21 | query { 22 | ... 23 | } 24 | `; 25 | 26 | export const TOPIC_SUBSCRIPTION = gql` 27 | subscription { 28 | ... 29 | } 30 | `; 31 | 32 | export const CREATE_TOPIC = gql` 33 | mutation { 34 | ... 35 | } 36 | `; 37 | ``` 38 | 39 | ```typescript 40 | // useTopic.hook.ts 41 | 42 | import { CREATE_TOPIC, TOPIC_SUBSCRIPTION, GET_TOPICS } from 'Queries/topics'; 43 | import { useMutation, useQuery, useSubscription } from '@apollo/client'; 44 | 45 | const useTopic = () => { 46 | const [createTopic, { data }] = useMutation(CREATE_TOPIC); 47 | const getTopics = () => useQuery(GET_TOPICS); 48 | const topicsSubscription = () => 49 | useSubscription(TOPIC_SUBSCRIPTION, {}, true); 50 | return { 51 | addTopic, 52 | getTopics, 53 | topicsSubscription, 54 | }; 55 | }; 56 | 57 | export default useTopic; 58 | ``` 59 | 60 | ```typescript 61 | // topics.model.ts 62 | 63 | import { useTopic } from 'Hooks'; 64 | 65 | const TopicModel = () => { 66 | const { addTopic, getTopics, topicSubscription } = useTopic(); 67 | 68 | // pass these as props to view 69 | const { loading, error, data } = getTopics(); 70 | const { sub_loading, sub_error, sub_data } = topicsSubscription(); 71 | ... 72 | }; 73 | ``` 74 | 75 | ### Config 76 | 77 | When fetching config, a context will need to be provided so that Apollo goes to the correct server. 78 | 79 | ```typescript 80 | // config/index.ts 81 | 82 | import gql from 'graphql-tag'; 83 | 84 | const GET_CONFIG = gql` 85 | query { 86 | ... 87 | } 88 | `; 89 | ``` 90 | 91 | ```typescript 92 | // config.hook.ts 93 | 94 | import { GET_CONFIG } from 'Queries/topics'; 95 | import { useQuery } from '@apollo/client'; 96 | 97 | const useConfig = () => { 98 | const getConfig = () => 99 | useQuery(GET_CONFIG, { 100 | context: { 101 | purpose: 'config', 102 | }, 103 | }); 104 | return getConfig; 105 | }; 106 | ``` 107 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This directory contains all client code for the Strimzi UI - ie code which is sent to a user's browser. A summary of contents can be found below: 4 | 5 | - [Bootstrap](./Bootstrap/README.md) - code and React components which are responsible for bootstrapping the UI 6 | - [Contexts](./Contexts/README.md) - state management code 7 | - [Elements](./Elements/README.md) - presentational React components 8 | - [Groups](./Groups/README.md) - React components which are combine and compose `Element` components 9 | - [Hooks](./Hooks/README.md) - custom reusable React Hooks 10 | - [Images](./Images/README.md) - images used across the UI 11 | - [Pages](./Pages/README.md) - metadata used to describe the pages shown in the UI 12 | - [Panels](./Panels/README.md) - section/page level components 13 | - [Queries](./Queries/README.md) - GraphQL request definitions (Queries, Mutations etc) 14 | - [Utils](./Utils/README.md) - common utility code used across the client UI 15 | - `tsconfig.json` - Typescript config for this codebase 16 | - `jest.config.js` - Jest config for this codebase. 17 | 18 | ## Configuration options 19 | 20 | The client codebase will include a significant number of configuration options, all of which [can be found here](../config/README.md). These values will be retrieved and made available via the [`ConfigFeatureFlag`](./Contexts/ConfigFeatureFlag/README.md) context at runtime, along with feature flag state. 21 | 22 | The below table details the top level items, and what they contain: 23 | 24 | | Configuration | Content | 25 | | ------------- | --------------------------------------------------------------------------- | 26 | | about | Key value pairs containing metadata about the UI - eg the version of the UI | 27 | -------------------------------------------------------------------------------- /client/Utils/README.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | This directory contains client utility code. It also is responsible for importing (and testing) of any general utility code from the root level `utils` folder, in the context of the client. 4 | 5 | Currently provided utilities are provided by utility type follows: 6 | 7 | - [`sanitise`](./sanitise/README.md) - set of helper functions to sanitise user input 8 | - [`window`](./window/README.md) - functions for interacting with the `Window` global object 9 | -------------------------------------------------------------------------------- /client/Utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './sanitise/sanitise'; 6 | export * from './window/window'; 7 | -------------------------------------------------------------------------------- /client/Utils/sanitise/README.md: -------------------------------------------------------------------------------- 1 | # sanitise 2 | 3 | A set of helper functions to sanitise/validate user input before said input is used across the UI. 4 | 5 | ## Functions available 6 | 7 | - `sanitiseUrlParams` - takes a given URL parameter type, eg `window.search`, and returns an object containing key value pairs of those parameters. If either the key or value do not pass a sanatisation check, they will be omitted from the returned object. Allowed characters are alphanumeric, as well as `.`,`,`,`_`,`-` and `=` characters. 8 | 9 | Example usage, where `window.search` is `param=true¶m2=false&foo=true`: 10 | 11 | ``` 12 | ... 13 | import {sanitiseUrlParams} from 'utils/sanitise' 14 | ... 15 | const { param, param2, foo } = sanitiseUrlParams(window.search); 16 | ... 17 | 18 | ``` 19 | 20 | In this case, `param2` and `foo` will be undefined, as it's key/value contained unsafe characters (`<`, `>`). 21 | -------------------------------------------------------------------------------- /client/Utils/sanitise/sanitise.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { sanitiseUrlParams } from './sanitise'; 6 | 7 | describe('sanitise function tests', () => { 8 | describe(`sanitiseUrlParams`, () => { 9 | [ 10 | { 11 | input: '', 12 | output: {}, 13 | }, 14 | { 15 | input: 'orphan', 16 | output: { orphan: '' }, 17 | }, 18 | { 19 | input: 'name=harry', 20 | output: { name: 'harry' }, 21 | }, 22 | { 23 | input: 'evil.=invalid', 24 | output: {}, 25 | }, 26 | { 27 | input: 'evil>=invalid', 28 | output: {}, 29 | }, 30 | { 31 | input: 'evil>=invalid&name=harry', 32 | output: { name: 'harry' }, 33 | }, 34 | ].forEach(({ input, output }) => 35 | it(`returns the expected response (${JSON.stringify( 36 | output 37 | )}) for given parameters '${input}'`, () => { 38 | expect(sanitiseUrlParams(input)).toEqual(output); 39 | }) 40 | ); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /client/Utils/sanitise/sanitise.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | // we explicitly only want `.` as an allowed character (vs `.`, matching any character) 6 | // eslint-disable-next-line no-useless-escape 7 | const allowedURLParamCharacters = new RegExp(/^[a-zA-Z0-9\.,_\-=]*$/); 8 | /** 9 | * sanitiseUrlParams returns an object containing all parameters names and values from a URL, with either names or values which do not match the `allowedURLParamCharacters` regex removed. The output of this function can thus be used safely, with potentially dangerous user input removed 10 | * @param urlParams - the params to parse, eg from `window.search` 11 | * @returns an object with the key being the parameter name, and the value being the value 12 | */ 13 | export const sanitiseUrlParams: ( 14 | urlParams: string 15 | ) => Record = (urlParams) => 16 | Array.from(new URLSearchParams(urlParams).entries()) // create an array of entries from the parsed params 17 | .filter( 18 | ([key, value]) => 19 | allowedURLParamCharacters.test(key) && 20 | allowedURLParamCharacters.test(value) 21 | ) // remove any entry where either the key or value does not pass the `allowedURLParamCharacters` regex 22 | .reduce( 23 | (acc, [key, value]) => ({ ...acc, [key]: value }), 24 | {} as Record 25 | ); // reduce into an object 26 | -------------------------------------------------------------------------------- /client/Utils/window/README.md: -------------------------------------------------------------------------------- 1 | # window 2 | 3 | A set of helper functions to interact with the global `Window` object. This keeps all window logic in one file, and enables easy stubbing for test purposes. 4 | 5 | ## Functions available 6 | 7 | - `getLocation` - returns the current `Location` object 8 | -------------------------------------------------------------------------------- /client/Utils/window/window.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { getLocation } from './window'; 6 | 7 | describe('window function tests', () => { 8 | describe(`getLocation`, () => { 9 | it('returns the location object from the window', () => { 10 | expect(getLocation()).toEqual(window.location); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /client/Utils/window/window.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | /** Returns the current location object from the global Window object */ 6 | export const getLocation: () => Location = () => window.location; 7 | -------------------------------------------------------------------------------- /client/i18n/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import i18n from 'i18next'; 6 | import { initReactI18next } from 'react-i18next'; 7 | import LanguageDetector from 'i18next-browser-languagedetector'; 8 | import { resources } from './locale'; 9 | // don't want to use this? 10 | // have a look at the Quick start guide 11 | // for passing in lng and translations on init 12 | 13 | const init = (): void => { 14 | i18n 15 | // pass the i18n instance to react-i18next. 16 | .use(initReactI18next) 17 | .use(LanguageDetector) 18 | // init i18next 19 | // for all options read: https://www.i18next.com/overview/configuration-options 20 | .init({ 21 | detection: { 22 | order: ['htmlTag', 'navigator'], 23 | caches: [], 24 | }, 25 | fallbackLng: 'en', 26 | debug: true, 27 | 28 | interpolation: { 29 | escapeValue: false, // not needed for react as it escapes by default 30 | }, 31 | resources, 32 | }); 33 | }; 34 | 35 | export { init }; 36 | -------------------------------------------------------------------------------- /client/i18n/locale/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "basic": "DE Welcome to the Strimzi UI", 4 | "insert": "DE This includes an {{insert}}", 5 | "formatted": "DE This includes formatting", 6 | "formattedInsert": "DE This includes a formatted {{insert}} and {{another}}", 7 | "customInserts": "DE This paragraph contains multiple inserts. First {{insert}} is a <0>div with a classname. Second is <1>bold. Third is <2>italic. Finally there is a <3>link.", 8 | "customContent": "DE This is <0>Something" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/i18n/locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "basic": "Welcome to the Strimzi UI", 4 | "insert": "This includes an {{insert}}", 5 | "formatted": "This includes formatting", 6 | "formattedInsert": "This includes a formatted {{insert}} and {{another}}", 7 | "customInserts": "This paragraph contains multiple inserts. First {{insert}} is a <0>div with a classname. Second is <1>bold. Third is <2>italic. Finally there is a <3>link.", 8 | "customContent": "This is <0>Something" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/i18n/locale/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import en from './en.json'; 6 | import de from './de.json'; 7 | 8 | export const resources = { 9 | en: { 10 | translation: en, 11 | }, 12 | de: { 13 | translation: de, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /client/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const merge = require('lodash.merge'); 6 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 7 | const { jestModuleMapper } = require('../utils/tooling/aliasHelper'); 8 | const { compilerOptions } = require('./tsconfig.json'); 9 | const commonConfig = require('../test_common/jest.common.config'); 10 | 11 | const config = { 12 | setupFilesAfterEnv: ['/../test_common/jest_rtl_setup.ts'], 13 | testMatch: ['/**/*.(spec|steps).[jt]s?(x)'], 14 | coverageDirectory: '/../coverage/client', 15 | moduleNameMapper: { 16 | ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 17 | ...jestModuleMapper, 18 | }, 19 | testEnvironment: 'jsdom', 20 | collectCoverageFrom: [ 21 | '**/*.{js,ts,jsx,tsx}', 22 | '!**/index.{js,ts,jsx,tsx}', 23 | '!**/*.steps.*', 24 | '!**/*.d.ts', 25 | '!**/*types.ts', 26 | '!**/*.assets.{ts,tsx}', 27 | '!jest.config.js', 28 | '!**/mock/**/*', 29 | // Wrapper around graphql - not something we need/wish to test 30 | '!Bootstrap/GraphQLClient/**', 31 | ], 32 | roots: ['', '/..'], 33 | }; 34 | 35 | module.exports = merge({}, commonConfig, config); 36 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "jsx": "react", 6 | "outDir": "../js/client", 7 | "baseUrl": ".", 8 | "paths": { 9 | "utils/*": ["../utils/*"], 10 | "ui-config/*": ["../config/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | linting/commitlint.config.js -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | This directory contains all configuration for both the client and server of the Strimzi-ui ([described here](../docs/Architecture.md#configuration-and-feature-flagging)). 4 | 5 | - `runtime.ts` - will contain all configuration options which require resolution at server runtime to return a value 6 | - `static.ts` - will contain all configuration options where values can be defined literally 7 | - `featureflags.ts` - will contain all feature flags. These could be defined literally, or require a callback to resolve (like `runtime.ts` configuration values) 8 | - `index.ts` - acts as a barrel file for all types, merging them together 9 | - `configHelpers.ts` - utility code for reducing the configuration, used in `index.ts` 10 | - `config.types.ts` - internal types for the config module 11 | - `types.ts` - all exported types for the config module 12 | 13 | ## Configuration sensitivity 14 | 15 | As mentioned in [the configuration architecture section](../docs/Architecture.md#configuration-and-feature-flagging), configuration will be reduced at runtime by the server, and then hosted via the [config server module](../server/config/README.md) for the client code to access. For the vast majority of configuration, the ability to access these values externally will not be an issue. However, some values will be sensitive, meaning they should be obfuscated or redacted entirely. To support this, configuration values can be typed as a [`programmaticValue`](./config.types.ts). Any `programmaticValue` which does not include a `publicConfigValue` key will be omitted from the publicly available configuration. 16 | -------------------------------------------------------------------------------- /config/config.types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | /** structure of processed configuration. Key value pairs, mapping to either a `T` or a nested `processedConfiguration` */ 7 | interface ProcessedConfiguration { 8 | [key: string]: T | ProcessedConfiguration; 9 | } 10 | 11 | /** external facing (ie for use in the UI) configuration object */ 12 | export type PublicConfig = { 13 | /** internal private configuration values */ 14 | values: Record | T>; 15 | /** public/external values. Will be exposed via the config module in the server */ 16 | publicValues: Record | T>; 17 | }; 18 | 19 | /** internal configuration definition type - an object of which its values will either be of type `ConfigValue`, or a sub instance of `Config` */ 20 | export interface Config { 21 | [key: string]: ConfigValue | Config; 22 | } 23 | 24 | /** internal configuration may need to be either evaluated at runtime, or present different values depending on context. `ProgrammaticValue` defines an interface allowing the definition of a dynamic (or static) value, which can then be replaced with another value if required */ 25 | export interface ProgrammaticValue { 26 | /** the value used/exported for 'real' use */ 27 | configValue: V | (() => V); 28 | /** the public value exposed if queried for. If not defined, no value will be accessible externally via the final `configurationType` */ 29 | publicConfigValue?: V | (() => V); 30 | } 31 | 32 | /** when defining configuration, the value can either be literal value of type `T`, or a `ProgrammaticValue`, where T is determined at runtime, or requires a different value to be exposed publicly*/ 33 | export type ConfigValue = T | ProgrammaticValue; 34 | 35 | /** a `Literal` configuration value in the UI config is either a string, number or boolean value */ 36 | export type Literal = string | number | boolean; 37 | -------------------------------------------------------------------------------- /config/configHelpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import merge from 'lodash.merge'; 7 | import { 8 | PublicConfig, 9 | ProgrammaticValue, 10 | Config, 11 | Literal, 12 | } from './config.types'; 13 | 14 | /** helper function to get the value of an environment variable, or a defined fallback value */ 15 | export const getEnvvarValue: ( 16 | name: string, 17 | fallback?: string 18 | ) => () => string = (name, fallback = `No value for ENVVAR ${name}`) => () => 19 | process.env[name] || fallback; 20 | 21 | /** helper used to process sets of `configurationDeclaration` or `featureFlagDeclaration` (D) to a `configurationType` with values being of type (T) for use across the codebase. 22 | * @param config - an array of `D` config decelerations. Important! If the same key is present in any `config`s provided in this array, the latter value will take precedence/be returned in the output 23 | * @returns a configuration object ready to use in across the UI 24 | */ 25 | export const processConfig = ( 26 | config: Config[] 27 | ): PublicConfig => { 28 | // merge all provided configs 29 | const mergedConfig = config.reduce>( 30 | (acc, thisConfig) => merge(acc, thisConfig), 31 | {} as Config 32 | ); 33 | 34 | // iterate through config 35 | const processedConfiguration: PublicConfig = Object.entries( 36 | mergedConfig 37 | ).reduce( 38 | (acc, [key, value]) => { 39 | const valueType = typeof value; 40 | const isLiteralValue = 41 | valueType === 'string' || 42 | valueType === 'boolean' || 43 | valueType === 'number'; 44 | const isConfigurationValue = 45 | !isLiteralValue && 46 | (value as ProgrammaticValue).configValue !== undefined; 47 | 48 | let publicValue, privateValue; 49 | 50 | if (isLiteralValue) { 51 | publicValue = value; 52 | privateValue = value; 53 | } else if (isConfigurationValue) { 54 | const { configValue, publicConfigValue } = value as ProgrammaticValue< 55 | T 56 | >; 57 | privateValue = 58 | typeof configValue === 'function' ? configValue() : configValue; 59 | publicValue = publicConfigValue; 60 | } else { 61 | const { values, publicValues } = processConfig([value as Config]); 62 | publicValue = publicValues; 63 | privateValue = values; 64 | } 65 | const { values, publicValues } = acc; 66 | return { 67 | values: { ...values, [key]: privateValue }, 68 | publicValues: { ...publicValues, [key]: publicValue }, 69 | }; 70 | }, 71 | { 72 | values: {}, 73 | publicValues: {}, 74 | } as PublicConfig 75 | ); 76 | 77 | return processedConfiguration; 78 | }; 79 | -------------------------------------------------------------------------------- /config/featureflags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { Config } from './config.types'; 6 | 7 | /** 8 | * feature flag configuration - used to enable/disable capabilities across the UI/server. The values can be static or dynamic, but the value must always be boolean 9 | */ 10 | 11 | export const featureFlags: Config = { 12 | client: { 13 | Home: { 14 | showVersion: true, 15 | }, 16 | Pages: { 17 | PlaceholderHome: true, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { processConfig } from './configHelpers'; 6 | import { featureFlags as rawFeatureFlags } from './featureflags'; 7 | import { client as runtimeClient, server as runtimeServer } from './runtime'; 8 | import { client as staticClient, server as staticServer } from './static'; 9 | 10 | export const featureFlags = processConfig([rawFeatureFlags]); 11 | export const client = processConfig([staticClient, runtimeClient]); 12 | export const server = processConfig([staticServer, runtimeServer]); 13 | -------------------------------------------------------------------------------- /config/runtime.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { Config, Literal } from './config.types'; 6 | import { getEnvvarValue } from './configHelpers'; 7 | 8 | /** 9 | * runtime configuration - values which can only be defined/evaluated at server runtime 10 | */ 11 | 12 | const client: Config = {}; 13 | 14 | const server: Config = { 15 | serverConfigPath: { 16 | configValue: getEnvvarValue('configPath', './server.config.json'), 17 | }, 18 | serverName: { 19 | configValue: getEnvvarValue('serverName', 'Strimzi-ui server'), 20 | }, 21 | }; 22 | 23 | export { client, server }; 24 | -------------------------------------------------------------------------------- /config/static.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { Config, Literal } from './config.types'; 7 | import { version as strimziUiVersion } from '../package.json'; 8 | 9 | /** 10 | * static configuration - literal values for the client and server that can be defined at develop time 11 | */ 12 | 13 | const client: Config = { 14 | about: { 15 | version: strimziUiVersion, 16 | }, 17 | }; 18 | 19 | const server: Config = { 20 | defaultConfig: { 21 | configValue: { 22 | authentication: { 23 | strategy: 'none', 24 | }, 25 | client: { 26 | configOverrides: {}, 27 | transport: {}, 28 | publicDir: './dist/client', 29 | }, 30 | featureFlags: {}, 31 | modules: { 32 | api: true, 33 | client: true, 34 | config: true, 35 | log: true, 36 | mockapi: false, 37 | }, 38 | proxy: { 39 | hostname: 'strimzi.admin.hostname.com', 40 | contextRoot: '/', 41 | port: 9080, 42 | transport: {}, 43 | }, 44 | session: { 45 | name: 'strimzi-ui', 46 | }, 47 | logging: {}, 48 | hostname: '0.0.0.0', 49 | port: 3000, 50 | }, 51 | }, 52 | }; 53 | 54 | export { client, server }; 55 | -------------------------------------------------------------------------------- /config/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { client, featureFlags, server } from './index'; 6 | 7 | export * from './config.types'; 8 | 9 | export type exposedClientType = typeof client.publicValues; 10 | export type exposedFeatureFlagsType = typeof featureFlags.publicValues; 11 | export type exposedServerType = typeof server.publicValues; 12 | -------------------------------------------------------------------------------- /cypress-cucumber-preprocessor.config.js: -------------------------------------------------------------------------------- 1 | e2e/cypress-cucumber-preprocessor.config.js -------------------------------------------------------------------------------- /docs/ContinuousIntegration.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration 2 | 3 | This repo makes use of GitHub Actions to run various checks to aid maintaining this repo. Actions allow us to run certain checks on PRs, maintain our dependencies, and have automatic issue checking. All code for workflows and actions can be found in the `.github` directory at the root of this repository. 4 | 5 | ## Automatic tests 6 | 7 | To make sure that master branch is alway safe, all of our tests for the UI are run on each pull request. For full details on our tests, [view our testing documentation](./Test.md). The output of the tests run by Jest can be viewed in the logs of the action on a pull request. For E2E tests, the results are also available in the logs, and the failure output will be uploaded as an artifact on the action. These can be downloaded via the `Artifacts` dropdown at the top right of the action page. 8 | 9 | ### Test coverage information 10 | 11 | As part of automatically running the tests, if they fail because of insufficient test coverage, the coverage report from the tests are formatted as a report on the pull request. This way, it is easy to see where the coverage requirements have not been met and is a quick way to let a contributer know what needs to be done. If the tests pass, no report will be given. 12 | 13 | ## Bundle size calculation 14 | 15 | It is important that we maintain as small as a bundle size as possible. To help with this, a bundle size report will be made on a pull request that shows the new size of the bundle and the percentage increase/decrease in size. By having this report, maintainers can easily raise questions if the size of the bundle changes dramatically. 16 | 17 | ## Linting 18 | 19 | All of our linting tools will run on a PR. To see what linting we enforce, [view our linting documentation](./Linting.md). These checks should not be an issue as they are automatically run when `git commit` is run on this repo. However, this prevents problems from anything that hasn't been checked in correctly. 20 | 21 | ## GitHub Pages 22 | 23 | On merge to master, a hosted version of the project storybook will be available to view on GitHub pages. 24 | 25 | ## Stale Issue/Pull Request Management 26 | 27 | To prevent this repo from filling with stale issues and pull requests, we make use of an action that will automatically flag old issues and pull requests as stale. If, after 30 days, the issue/pr is still open, it will automatically be closed. The action that manages this can be found [here](https://github.com/marketplace/actions/close-stale-issues). An issue/pr will become stale after 14 days of no activity. If there is no activity for a further 5 days, it will be deleted. 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This directory contains all documentation relating to the whole repository. For convenience, links to all documentation are available below: 4 | 5 | ## Contents 6 | 7 | - [Architecture](./Architecture.md) 8 | - [Build](./Build.md) 9 | - [Linting](./Linting.md) 10 | - [Test](./Test.md) 11 | - [Contribution](./Contribution.md) 12 | - [Continuous Integration](./ContinuousIntegration.md) 13 | -------------------------------------------------------------------------------- /docs/assets/DevelopmentTopology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strimzi/strimzi-ui/962a778df265ac2863a4ad82e55cd9db5841a8fb/docs/assets/DevelopmentTopology.png -------------------------------------------------------------------------------- /docs/assets/DevelopmentTopologyUsingRealStrimziAdmin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strimzi/strimzi-ui/962a778df265ac2863a4ad82e55cd9db5841a8fb/docs/assets/DevelopmentTopologyUsingRealStrimziAdmin.png -------------------------------------------------------------------------------- /docs/assets/EndToEndTopology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strimzi/strimzi-ui/962a778df265ac2863a4ad82e55cd9db5841a8fb/docs/assets/EndToEndTopology.png -------------------------------------------------------------------------------- /docs/assets/ProductionTopology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strimzi/strimzi-ui/962a778df265ac2863a4ad82e55cd9db5841a8fb/docs/assets/ProductionTopology.png -------------------------------------------------------------------------------- /docs/assets/StrimziUIDiagrams.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strimzi/strimzi-ui/962a778df265ac2863a4ad82e55cd9db5841a8fb/docs/assets/StrimziUIDiagrams.pptx -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # End to End testing 2 | 3 | This directory contains everything you need to write End-to-End tests. Please find documentation for E2E testing [here](../../docs/Test.md#end-to-end-testing). 4 | 5 | ## Contents 6 | 7 | - [features](./features/README.md) - this directory contains all of the cucumber feature files. It has a subdirectory called `step_definitions` that contains all of the step definitions for those feature files. 8 | - [plugins](./plugins/README.md) - this directory contains a single `index.ts` file where all cypress plugins are imported and attatched. If a new plugin needs to be added, this is where it needs to be installed. 9 | - [support](./support/README.md) - this directory contains a `index.ts` file that is imported by cypress globally so anything defined here can be used across all tests. 10 | -------------------------------------------------------------------------------- /e2e/cypress-cucumber-preprocessor.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | module.exports = { 6 | stepDefinitions: 'e2e/features/step_definitions', 7 | }; 8 | -------------------------------------------------------------------------------- /e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "screenshotsFolder": "e2e/failure_output/screenshots", 4 | "videosFolder": "e2e/failure_output/videos", 5 | "integrationFolder": "e2e/features", 6 | "fixturesFolder": "e2e/fixtures", 7 | "supportFile": "e2e/support/index.ts", 8 | "pluginsFile": "e2e/plugins/index.ts", 9 | "testFiles": "**/*.feature" 10 | } 11 | -------------------------------------------------------------------------------- /e2e/features/README.md: -------------------------------------------------------------------------------- 1 | # Cypress features 2 | 3 | This directory is home to all Cypress `.feature` files. Each feature should focus on a suite of scenarios for testing - but not for testing specific components. All step definitions for these features go in the `step_definitions` directory. Where possible, step definitions should be as generic and re-usable as possible. 4 | -------------------------------------------------------------------------------- /e2e/features/coreUX.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: Strimzi-ui core UX 4 | 5 | Scenario: When a user accesses the Strimzi-ui, they see the home page 6 | Given I am on the strimzi-ui homepage 7 | Then the welcome message appears 8 | And version information about this UI is displayed -------------------------------------------------------------------------------- /e2e/features/step_definitions/coreUX.stepdef.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { And, Given, Then } from 'cypress-cucumber-preprocessor/steps'; 6 | 7 | Given('I am on the strimzi-ui homepage', () => { 8 | cy.visit('localhost:3000/index.html'); 9 | }); 10 | 11 | Then(`the welcome message appears`, () => { 12 | cy.get('#root').contains(`Welcome to the Strimzi UI`); 13 | }); 14 | 15 | And(`version information about this UI is displayed`, () => { 16 | // validates the context/server config modules work - exact step to change in future 17 | cy.get('#root').contains(`Version: 0.0.1`); 18 | }); 19 | -------------------------------------------------------------------------------- /e2e/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Cypress Plugins 2 | 3 | This directory holds an `index.ts` file that Cypress uses to load any plugins for e2e tests. If a new plugin is installed, 4 | this is where it will need to be set up. See [Cypress plugins](https://docs.cypress.io/guides/tooling/plugins-guide.html). 5 | -------------------------------------------------------------------------------- /e2e/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { default as cucumber } from 'cypress-cucumber-preprocessor'; 6 | 7 | module.exports = (on: (evt: string, callback: () => void) => void) => { 8 | const options = { 9 | typescript: require.resolve('typescript'), 10 | }; 11 | 12 | const cucumberPreProcessor = cucumber.default; 13 | 14 | on('file:preprocessor', cucumberPreProcessor(options)); 15 | }; 16 | -------------------------------------------------------------------------------- /e2e/support/README.md: -------------------------------------------------------------------------------- 1 | # Cypress support 2 | 3 | This directory contains an `index.ts` file that will run before every Cypress features. This can be useful for having global commands or setup that needs to be run before any test. See [Cypress support file](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Support-file). 4 | -------------------------------------------------------------------------------- /e2e/support/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["cypress", "node"] 5 | }, 6 | "include": ["./**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /husky.config.js: -------------------------------------------------------------------------------- 1 | linting/husky.config.js -------------------------------------------------------------------------------- /linting/.eslintignore: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | # eslint ignore file - files/directories to be excluded 4 | 5 | # negated files/directories to be included 6 | # Directories that start with dots are excluded by default, causing eslint warnings 7 | !.storybook/ 8 | !.github/ 9 | dist 10 | -------------------------------------------------------------------------------- /linting/.prettierignore: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | # prettier ignore file - files/directories to be excluded 4 | dist/ 5 | generated/ 6 | coverage/ 7 | .* 8 | *.txt 9 | *.feature 10 | dockerfile 11 | *.conf 12 | *.sh 13 | *.svg -------------------------------------------------------------------------------- /linting/README.md: -------------------------------------------------------------------------------- 1 | # Linting 2 | 3 | This directory contains code and configuration required to lint this codebase, as per [the linting documentation](../docs/Linting.md). Some of the tools used require configuration to exist at the root of the project. In these cases, symlinks are used to reference the actual configuration in this directory. All files in this directory are named to indicate what tool they are used with, and will contain comments to reference how they implement/enable the configuration desired. 4 | -------------------------------------------------------------------------------- /linting/commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | // https://commitlint.js.org/#/reference-rules 7 | const config = { 8 | 'signed-off-by': [2, 'always', 'Signed-off-by:'], 9 | }; 10 | 11 | module.exports = { 12 | rules: config, 13 | }; 14 | -------------------------------------------------------------------------------- /linting/eslint.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const rulesets = [ 6 | 'eslint:recommended', 7 | 'plugin:react/recommended', 8 | 'plugin:react-hooks/recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ]; 12 | 13 | const customRules = { 14 | // prefer spaces (2) over tabs for indentation - tab width can be changed, space cannot 15 | indent: ['error', 2], 16 | // all lines to have semicolons to end statements 17 | semi: ['error', 'always'], 18 | }; 19 | 20 | // https://eslint.org/docs/user-guide/configuring 21 | // config for all code written in js 22 | module.exports = { 23 | env: { 24 | browser: true, 25 | node: true, 26 | es6: true, 27 | 'jest/globals': true, 28 | 'cypress/globals': true, 29 | }, 30 | // extend recommended rule sets - combine with prettier config, which must go last to work properly 31 | extends: rulesets.concat(['prettier']), 32 | parser: '@typescript-eslint/parser', 33 | parserOptions: { 34 | ecmaVersion: 2020, 35 | ecmaFeatures: { 36 | jsx: true, 37 | }, 38 | sourceType: 'module', 39 | }, 40 | plugins: ['react', 'jest', 'cypress', '@typescript-eslint'], 41 | // detect and use the version of react installed to guide rules used 42 | settings: { 43 | react: { 44 | version: 'detect', 45 | }, 46 | }, 47 | rules: customRules, 48 | overrides: [ 49 | { 50 | files: ['**/*.tsx'], 51 | rules: { 52 | 'react/prop-types': 'off', 53 | }, 54 | }, 55 | { 56 | files: ['**/*.js'], //Allow commonjs modules for js files 57 | rules: { 58 | '@typescript-eslint/no-var-requires': 'off', 59 | }, 60 | }, 61 | ], 62 | }; 63 | -------------------------------------------------------------------------------- /linting/husky.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | // https://github.com/typicode/husky#supported-hooks 7 | const config = { 8 | 'pre-commit': 'lint-staged --config ./linting/lint-staged.config.js', 9 | 'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS', 10 | }; 11 | 12 | module.exports = { 13 | hooks: config, 14 | }; 15 | -------------------------------------------------------------------------------- /linting/license-check-and-add.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "./utils/tooling/headers/StrimziHeader.txt", 3 | "ignore": [ 4 | ".**", 5 | ".git/**", 6 | "./utils/headers", 7 | "**.md", 8 | "**.pptx", 9 | "./build/dockerfile", 10 | "./generated/**", 11 | "./coverage/**", 12 | "./.idea/**" 13 | ], 14 | "licenseFormats": { 15 | "tsx|jsx|ts|js|css|scss": { 16 | "prepend": "/*", 17 | "append": " */", 18 | "eachLine": { 19 | "prepend": " * " 20 | } 21 | }, 22 | "html|svg": { 23 | "prepend": "", 25 | "eachLine": { 26 | "prepend": " " 27 | } 28 | }, 29 | "dotfiles|prettierignore|feature|conf": { 30 | "eachLine": { 31 | "prepend": "# " 32 | } 33 | } 34 | }, 35 | "defaultFormat": { 36 | "eachLine": { 37 | "prepend": "# " 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /linting/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | // lint-staged will call these scripts with a set of files - eg npm run x 'file.txt' 'styling.css' etc 7 | const lintChecksForAllFiles = ['npm run lint:allfiles', 'npm run lint:format']; // note: the licence tool, run as a part of `allfiles`, will update non staged files as well - this cannot be configured currently. 8 | const lintChecksForSrcFiles = ['npm run lint:src']; 9 | const lintChecksForStylingFiles = ['npm run lint:styling']; 10 | 11 | // https://github.com/okonet/lint-staged#configuration 12 | module.exports = { 13 | '**': lintChecksForAllFiles, 14 | '**.js|**.ts|**.tsx': lintChecksForSrcFiles, 15 | '**.(s)css': lintChecksForStylingFiles, 16 | }; 17 | -------------------------------------------------------------------------------- /linting/prettier.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | // https://prettier.io/docs/en/options.html 7 | const customRules = { 8 | semi: true, 9 | tabWidth: 2, // use 2 spaces for indentation - not an ideal variable name 10 | singleQuote: true, 11 | jsxSingleQuote: true, 12 | }; 13 | 14 | module.exports = { 15 | ...customRules, 16 | }; 17 | -------------------------------------------------------------------------------- /linting/stylelint.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | const rulesets = [ 7 | 'stylelint-config-standard', 8 | 'stylelint-config-recommended', 9 | 'stylelint-config-sass-guidelines', 10 | ]; 11 | 12 | // see https://github.com/stylelint/awesome-stylelint for config 13 | module.exports = { 14 | // extend recommended rule sets - combine with prettier config, which must go last to work properly 15 | extends: rulesets.concat(['stylelint-config-prettier']), 16 | // ignore built files 17 | ignoreFiles: ['**/dist'], 18 | plugins: ['stylelint-selector-bem-pattern'], 19 | rules: { 20 | 'plugin/selector-bem-pattern': { 21 | componentName: '[A-Z]+', 22 | componentSelectors: { 23 | initial: '^\\.{componentName}(?:-[a-z]+)?$', 24 | combined: '^\\.combined-{componentName}-[a-z]+$', 25 | }, 26 | utilitySelectors: '^\\.util-[a-z]+$', 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /server/api/README.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | This module proxies any requests made to `/api` to the configured backend server defined in the server configuration. It also handles error conditions, if any were to occur during the proxy process. 4 | -------------------------------------------------------------------------------- /server/api/api.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: api module 4 | 5 | Behaviours and capabilities provided by the api module 6 | 7 | Scenario: Proxies all requests made to /api to the configured backend 8 | Given a 'api_only' server configuration 9 | And I run an instance of the Strimzi-UI server 10 | When I make a 'get' request to '/api/foo' 11 | Then I make the expected proxy request and get the expected proxied response 12 | 13 | Scenario: Proxies all requests made to /api to the securley configured backend 14 | Given a 'api_secured_only' server configuration 15 | And I run an instance of the Strimzi-UI server 16 | When I make a 'get' request to '/api/foo' 17 | Then I make the expected proxy request and get the expected proxied response 18 | 19 | Scenario: Handles errors from the proxied backend gracefully 20 | Given a 'api_secured_only' server configuration 21 | And the backend proxy returns an error response 22 | And I run an instance of the Strimzi-UI server 23 | When I make a 'get' request to '/api/foo' 24 | Then I make the expected proxy request and get the expected proxied response 25 | 26 | Scenario: Proxies all requests made to /api to the specified context root 27 | Given a 'api_with_custom_context_root' server configuration 28 | And I run an instance of the Strimzi-UI server 29 | When I make a 'get' request to '/api/foo' 30 | Then I make the expected proxy request and get the expected proxied response -------------------------------------------------------------------------------- /server/api/controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { strimziUIRequestType, strimziUIResponseType } from 'types'; 6 | 7 | export const proxyErrorHandler: ( 8 | err: Error, 9 | req: strimziUIRequestType, 10 | res: strimziUIResponseType 11 | ) => void = (err, req, res) => { 12 | res.locals.strimziuicontext.logger.debug( 13 | { err }, 14 | `Error occurred whilst proxying request '${req.url}'. ${err.message}` 15 | ); 16 | res.sendStatus(500); 17 | }; 18 | 19 | export const proxyStartHandler: ( 20 | proxyReq: unknown, 21 | req: strimziUIRequestType, 22 | res: strimziUIResponseType 23 | ) => void = (_, req, res) => { 24 | res.locals.strimziuicontext.logger.debug( 25 | `Proxying request '${req.url}' to the backend api` 26 | ); 27 | }; 28 | 29 | export const proxyCompleteHandler: ( 30 | proxyRes: { 31 | statusCode: number; 32 | statusMessage: string; 33 | }, 34 | req: strimziUIRequestType, 35 | res: strimziUIResponseType 36 | ) => void = ({ statusCode, statusMessage }, req, res) => { 37 | res.locals.strimziuicontext.logger.debug( 38 | `Response from backend api for request '${req.url}' : ${statusCode} - ${statusMessage}` 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /server/api/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { ApiModule } from './router'; 6 | -------------------------------------------------------------------------------- /server/api/router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import express from 'express'; 6 | import { createProxyServer } from 'http-proxy'; 7 | import { UIServerModule } from 'types'; 8 | 9 | import { 10 | proxyErrorHandler, 11 | proxyCompleteHandler, 12 | proxyStartHandler, 13 | } from './controller'; 14 | 15 | const moduleName = 'api'; 16 | 17 | export const ApiModule: UIServerModule = { 18 | moduleName, 19 | addModule: (logger, authFn, serverConfig) => { 20 | const { proxy } = serverConfig; 21 | const { exit } = logger.entry('addModule', proxy); 22 | const { hostname, port, contextRoot, transport } = proxy; 23 | const { cert, minTLS } = transport; 24 | 25 | const proxyConfig = { 26 | target: `${cert ? 'https' : 'http'}://${hostname}:${port}${contextRoot}`, 27 | ca: cert, 28 | minVersion: minTLS, 29 | changeOrigin: true, 30 | secure: cert ? true : false, 31 | }; 32 | 33 | logger.debug({ proxyConfig }, `api proxy configuration`); 34 | 35 | const routerForModule = express.Router(); 36 | const backendProxy = createProxyServer(proxyConfig); 37 | 38 | // add proxy event handlers 39 | backendProxy.on('error', proxyErrorHandler); 40 | backendProxy.on('proxyReq', proxyStartHandler); 41 | backendProxy.on('proxyRes', proxyCompleteHandler); 42 | // proxy all requests post auth check 43 | routerForModule.all('*', authFn, (req, res) => backendProxy.web(req, res)); 44 | 45 | return exit({ mountPoint: '/api', routerForModule }); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /server/client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | This module is responsible for serving the built client code, and does so on the `/` context root. Given the number of built files, and that some files represent pages that require privileged access, an include list of `public` files, I.e files any user could access without need of authentication, is defined in the `controller` as `publicFiles`. All other files will require a user to pass the authentication challenge. Examples of `public` files include images, fonts, as well as `index.html` as the bootstrap for the client. Content served by this module resides by default in `/dist/client`, but is configurable via `client.publicDir` provided via server config. 4 | 5 | It also includes a behaviour if a request is not matched/served, it will redirect to `/index.html`, as long as an `index.html` file is present in the built output. 6 | -------------------------------------------------------------------------------- /server/client/client.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: client module 4 | 5 | Behaviours and capabilities provided by the client module 6 | 7 | Scenario Outline: If no asset can be served, the client module returns 404 8 | Given a 'client_only' server configuration 9 | And There are no files to serve 10 | And Authentication is required 11 | And I run an instance of the Strimzi-UI server 12 | When I make a 'get' request to '' 13 | Then I get the expected status code '' response 14 | 15 | Examples: 16 | | Asset | StatusCode | 17 | | /index.html | 404 | 18 | | /images/picture.svg | 404 | 19 | | /doesnotexist.html | 404 | 20 | | /someroute | 404 | 21 | | /protected.html | 404 | 22 | | / | 404 | 23 | 24 | Scenario Outline: If assets can be served, the client module returns the appropriate return code for a request of 25 | Given a 'client_only' server configuration 26 | And There are files to serve 27 | And Authentication is required 28 | And I run an instance of the Strimzi-UI server 29 | When I make a 'get' request to '' 30 | Then I get the expected status code '' response 31 | # if the route (not file) is not matched, we redirect to index.html. Hence / and someroute response 32 | Examples: 33 | | Asset | StatusCode | 34 | | /index.html | 200 | 35 | | /images/picture.svg | 200 | 36 | | /doesnotexist.html | 404 | 37 | | /someroute | 302 | 38 | | /protected.html | 511 | 39 | | / | 200 | 40 | 41 | 42 | Scenario: Critical configuration is templated into index.html so the client can bootstrap 43 | Given a 'client_only' server configuration 44 | And There are files to serve 45 | And Authentication is required 46 | And I run an instance of the Strimzi-UI server 47 | When I make a 'get' request to '/index.html' 48 | Then the file is returned as with the expected configuration included 49 | -------------------------------------------------------------------------------- /server/client/client.steps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import merge from 'lodash.merge'; 6 | import { And, Then, Fusion } from 'jest-cucumber-fusion'; 7 | import { 8 | stepWithWorld, 9 | stepWhichUpdatesWorld, 10 | } from 'test_common/commonServerSteps'; 11 | 12 | And( 13 | 'There are no files to serve', 14 | stepWhichUpdatesWorld((world) => { 15 | return { 16 | ...world, 17 | configuration: merge(world.configuration, { 18 | client: { 19 | publicDir: '/dir/that/does/not/exist', 20 | }, 21 | }), 22 | }; 23 | }) 24 | ); 25 | 26 | And('There are files to serve', () => { 27 | // NO_OP - the `client_only` configuration is already configured to serve fixture files 28 | }); 29 | 30 | Then( 31 | /I get the expected status code '(.+)' response/, 32 | stepWithWorld(async (world, statusCode) => { 33 | const { request } = world; 34 | await request.then( 35 | (res) => { 36 | const { status } = res; 37 | const expectedStatus = parseInt(statusCode as string); 38 | expect(status).toBe(expectedStatus); 39 | }, 40 | (err) => { 41 | throw err; 42 | } 43 | ); 44 | }) 45 | ); 46 | 47 | Then( 48 | 'the file is returned as with the expected configuration included', 49 | stepWithWorld(async (world) => { 50 | const { request, configuration } = world; 51 | const configuredAuthType = configuration.authentication.strategy; 52 | 53 | await request.then( 54 | (res) => { 55 | const { status, text } = res; 56 | // pull required configuration from the world, and create the expected shape from them 57 | const expectedConfig = encodeURIComponent( 58 | JSON.stringify({ authType: configuredAuthType }) 59 | ); 60 | expect(status).toBe(200); 61 | expect(text.includes(expectedConfig)).toBe(true); 62 | }, 63 | (err) => { 64 | throw err; 65 | } 66 | ); 67 | }) 68 | ); 69 | 70 | Fusion('client.feature'); 71 | -------------------------------------------------------------------------------- /server/client/controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { resolve, sep } from 'path'; 7 | import { existsSync, readdirSync, readFileSync } from 'fs'; 8 | import { serverConfigType } from 'types'; 9 | import { render } from 'mustache'; 10 | 11 | // function to recursively get all files from a directory 12 | const getFilesInDirectory: (directory: string) => Array = (directory) => 13 | existsSync(directory) 14 | ? readdirSync(directory, { withFileTypes: true }).reduce((acc, fileObj) => { 15 | return fileObj.isFile() 16 | ? acc.concat([`${directory}${sep}${fileObj.name}`]) 17 | : acc.concat( 18 | getFilesInDirectory(`${directory}${sep}${fileObj.name}`) 19 | ); 20 | }, [] as string[]) 21 | : []; 22 | 23 | // mark a subset of files as public - this means any user can access them. These entries will be used in a regex - if the test passes, it will be considered public 24 | const publicFiles = [ 25 | 'images/*', 26 | 'fonts/*', 27 | 'favicon.ico', 28 | 'index.html', 29 | 'main.css', 30 | 'main.bundle.js', 31 | 'main.bundle.js.gz', 32 | ]; 33 | 34 | export const getFiles: ( 35 | publicDirectory: string 36 | ) => { 37 | totalNumberOfFiles: number; 38 | hasIndexFile: boolean; 39 | indexFile: string; 40 | protectedFiles: Array; 41 | builtClientDir: string; 42 | } = (publicDirectory) => { 43 | const builtClientDir = resolve(publicDirectory); 44 | 45 | // get all files in the client directory 46 | const allFilesInClientDirectory = getFilesInDirectory( 47 | builtClientDir 48 | ).map((fileNameAndPath) => 49 | fileNameAndPath.substr(builtClientDir.length, fileNameAndPath.length) 50 | ); 51 | 52 | // get a list of all files that do not match the above listed public file regex set 53 | const protectedFiles = allFilesInClientDirectory.reduce( 54 | (acc, file) => 55 | !publicFiles.some((publicFile) => new RegExp(publicFile).test(file)) 56 | ? acc.concat([file]) 57 | : acc, 58 | [] as string[] 59 | ); 60 | 61 | const hasIndexFile = allFilesInClientDirectory.includes('/index.html'); 62 | const indexFile = hasIndexFile 63 | ? readFileSync(resolve(`${builtClientDir}${sep}index.html`), { 64 | encoding: 'utf-8', 65 | }) 66 | : ''; 67 | 68 | return { 69 | totalNumberOfFiles: allFilesInClientDirectory.length, 70 | hasIndexFile, 71 | indexFile, 72 | protectedFiles: protectedFiles, 73 | builtClientDir, 74 | }; 75 | }; 76 | 77 | export const renderTemplate: (indexTemplate: string) => (req, res) => void = ( 78 | indexTemplate 79 | ) => (req, res) => { 80 | const { entry, debug } = res.locals.strimziuicontext.logger; 81 | const { exit } = entry('renderTemplate'); 82 | const { authentication } = res.locals.strimziuicontext 83 | .config as serverConfigType; 84 | const bootstrapConfigs = { 85 | authType: authentication.strategy, 86 | }; 87 | debug('Templating bootstrap config containing %o', bootstrapConfigs); 88 | res.send( 89 | exit( 90 | render(indexTemplate, { 91 | bootstrapConfigs: encodeURIComponent(JSON.stringify(bootstrapConfigs)), 92 | }) 93 | ) 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /server/client/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { ClientModule } from './router'; 6 | -------------------------------------------------------------------------------- /server/client/router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import express from 'express'; 6 | import expressStaticGzip from 'express-static-gzip'; 7 | import { getFiles, renderTemplate } from './controller'; 8 | import { UIServerModule } from 'types'; 9 | 10 | const moduleName = 'client'; 11 | 12 | export const ClientModule: UIServerModule = { 13 | moduleName, 14 | addModule: (logger, authFn, serverConfig) => { 15 | const { publicDir } = serverConfig.client; 16 | const { exit } = logger.entry('addModule', publicDir); 17 | const routerForModule = express.Router(); 18 | 19 | const { 20 | totalNumberOfFiles, 21 | protectedFiles, 22 | builtClientDir, 23 | hasIndexFile, 24 | indexFile, 25 | } = getFiles(publicDir); 26 | 27 | logger.debug( 28 | `Client is hosting ${totalNumberOfFiles} static files, ${protectedFiles.length} of which are protected and require authentication` 29 | ); 30 | logger.debug(`Client has index.html to serve? ${hasIndexFile}`); 31 | 32 | // add the auth middleware to all non public files 33 | protectedFiles.forEach((file) => routerForModule.get(`${file}`, authFn)); 34 | 35 | // return index.html, with configuration templated in 36 | hasIndexFile && 37 | routerForModule.get('/index.html', renderTemplate(indexFile)); 38 | 39 | // host all files from the client dir 40 | routerForModule.get( 41 | '*', 42 | expressStaticGzip(builtClientDir, {}), 43 | express.static(builtClientDir, { index: false }) 44 | ); 45 | 46 | // if no match, not a file (path contains '.'), and we have an index.html file, redirect to it (ie return index so client navigation logic kicks in). Else do nothing (404 unless another module handles it) 47 | hasIndexFile && 48 | routerForModule.get(/^((?!\.).)+$/, (req, res) => 49 | res.redirect(`/index.html`) 50 | ); 51 | 52 | return exit({ mountPoint: '/', routerForModule }); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /server/config/README.md: -------------------------------------------------------------------------------- 1 | # config 2 | 3 | This module is responsible for serving the current configuration to the client on request. It does this by providing a GraphQL endpoint `/config` which exposes ['all non sensitive'](../../config/README.md#configuration-sensitivity) configuration from the [`config` directory](../../config). The schema is dynamically generated by [`json-to-simple-graphql-schema`](https://github.com/walmartlabs/json-to-simple-graphql-schema). 4 | -------------------------------------------------------------------------------- /server/config/config.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: config module 4 | 5 | Behaviours and capabilities provided by the config module 6 | 7 | Scenario: Returns with the expected response for a config call 8 | Given a 'config_only' server configuration 9 | And I run an instance of the Strimzi-UI server 10 | When I make a 'getConfigAndFeatureFlagQuery' gql request to '/config' 11 | Then I get the expected config response 12 | 13 | Scenario: Returns with the expected response for a config call when config and feature flag overrides are present in the server configuration 14 | Given a 'config_only_with_config_overrides' server configuration 15 | And I run an instance of the Strimzi-UI server 16 | When I make a 'getConfigAndFeatureFlagQueryWithConfigOverrides' gql request to '/config' 17 | Then I get the expected config response with the config overrides present -------------------------------------------------------------------------------- /server/config/config.steps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { Then, Fusion } from 'jest-cucumber-fusion'; 6 | import { stepWithWorld } from 'test_common/commonServerSteps'; 7 | 8 | Then( 9 | 'I get the expected config response', 10 | stepWithWorld((world) => { 11 | const { request } = world; 12 | return request.expect(200).expect((res) => { 13 | const { data } = res.body; 14 | expect(data).not.toBeUndefined(); 15 | 16 | const { client, server, featureFlags } = data; 17 | 18 | // confirm for all three config types the generated type names are present - shows the schema generation and resolvers are working 19 | expect(client).not.toBeUndefined(); 20 | expect(client._generatedTypeName).toBe('client'); 21 | 22 | expect(server).not.toBeUndefined(); 23 | expect(server._generatedTypeName).toBe('server'); 24 | 25 | expect(featureFlags).not.toBeUndefined(); 26 | expect(featureFlags._generatedTypeName).toBe('featureFlags'); 27 | }); 28 | }) 29 | ); 30 | 31 | Then( 32 | 'I get the expected config response with the config overrides present', 33 | stepWithWorld((world) => { 34 | const { request } = world; 35 | return request.expect(200).expect((res) => { 36 | const { data } = res.body; 37 | 38 | expect(data).not.toBeUndefined(); 39 | 40 | const { client, server, featureFlags } = data; 41 | 42 | // confirm for all values are as expected - values defined in testConfig.ts 43 | expect(client).not.toBeUndefined(); 44 | expect(client.version).toBe('34.0.0'); 45 | 46 | expect(server).not.toBeUndefined(); 47 | expect(server._generatedTypeName).toBe('server'); 48 | 49 | expect(featureFlags).not.toBeUndefined(); 50 | expect(featureFlags.client.Home.showVersion).toBe(false); // overwrite a value from config 51 | expect(featureFlags.testFlag).toBe(true); 52 | }); 53 | }) 54 | ); 55 | 56 | Fusion('config.feature'); 57 | -------------------------------------------------------------------------------- /server/config/controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import merge from 'lodash.merge'; 7 | import { jsonToSchema } from '@walmartlabs/json-to-simple-graphql-schema/lib'; 8 | import { IResolvers } from 'apollo-server-express'; 9 | import { serverConfigType } from 'types'; 10 | import { featureFlags, client, server } from 'ui-config/index'; 11 | 12 | type configSchemaResolverType = { 13 | typeDefs: string; 14 | resolvers: IResolvers; 15 | }; 16 | 17 | export const apolloConfig: ( 18 | config: serverConfigType 19 | ) => configSchemaResolverType = (config) => { 20 | const { 21 | featureFlags: featureFlagOverrides, 22 | client: { configOverrides }, 23 | } = config; 24 | 25 | const configToHost = { 26 | featureFlags: merge({}, featureFlags.publicValues, featureFlagOverrides), 27 | client: merge({}, client.publicValues, configOverrides), 28 | server: server.publicValues, 29 | }; 30 | 31 | const { query, schemaTypes, resolvers } = Object.entries(configToHost) 32 | .map(([key, config]) => ({ 33 | type: key, 34 | typeValue: { _generatedTypeName: key, ...config }, 35 | })) 36 | .reduce( 37 | ({ schemaTypes, resolvers }, { type, typeValue }) => { 38 | const { value } = jsonToSchema({ 39 | baseType: type, 40 | jsonInput: JSON.stringify(typeValue), 41 | }); 42 | const allResolvers = { ...resolvers.Query, [type]: () => typeValue }; 43 | 44 | return { 45 | schemaTypes: schemaTypes.concat([value]), 46 | resolvers: { 47 | Query: { 48 | ...allResolvers, 49 | }, 50 | }, 51 | query: `type Query { ${Object.keys(allResolvers).reduce( 52 | (acc, key) => `${acc}${key}: ${key} `, 53 | '' 54 | )}} `, 55 | }; 56 | }, 57 | { 58 | query: ``, 59 | schemaTypes: [] as Array, 60 | resolvers: { Query: {} }, 61 | } 62 | ); 63 | 64 | const typeDefs = `${query}${schemaTypes.join(' ')}`; 65 | return { typeDefs, resolvers }; 66 | }; 67 | -------------------------------------------------------------------------------- /server/config/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { ConfigModule } from './router'; 6 | -------------------------------------------------------------------------------- /server/config/router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import express from 'express'; 6 | import { UIServerModule } from 'types'; 7 | 8 | import bodyParser from 'body-parser'; 9 | import { ApolloServer } from 'apollo-server-express'; 10 | import { apolloConfig } from './controller'; 11 | 12 | const moduleName = 'config'; 13 | 14 | export const ConfigModule: UIServerModule = { 15 | moduleName, 16 | addModule: (logger, authFn, config) => { 17 | const { exit } = logger.entry('addModule'); 18 | const routerForModule = express.Router(); 19 | 20 | const server = new ApolloServer({ 21 | ...apolloConfig(config), 22 | }); 23 | 24 | routerForModule.use( 25 | authFn, 26 | bodyParser.json(), 27 | server.getMiddleware({ path: '/' }) 28 | ); 29 | 30 | return exit({ mountPoint: '/config', routerForModule }); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /server/core/README.md: -------------------------------------------------------------------------------- 1 | # core 2 | 3 | This module represents the core Express server. It also imports, configures and provides common items and capabilities to all modules, such as an authentication check middleware and logging utilities, as well as common middleware and security features. 4 | 5 | ## Public api 6 | 7 | The core `app.ts` file (in place of `router.ts`) exports one function, `returnExpress`. This function takes one parameter, being a callback to get the current configuration. This is expected to be used in `main.ts` to bind the returned express app to an http(s) server. 8 | 9 | ## Interaction with other modules 10 | 11 | The core module will import and interact with all other modules to implement the server at run time. Thus, there are two points where the core module will interact with the other modules. These are detailed below. 12 | 13 | ### Import time 14 | 15 | The core module will import all modules' default export. The export is expected to contain two keys - `moduleName` and `addModule`, as per the `UIServerModule` interface. `addModule` is expected to be a function, which takes the following parameters: 16 | 17 | ``` 18 | const { mountPoint, routerFromModule } = myModule(logGenerator, authMiddleware, serverConfig); 19 | ``` 20 | 21 | Where: 22 | 23 | - `logGenerator` is a function which will return a logger to be used in `addModule` only to trace entry, exit and any helpful diagnostics while this module mounts 24 | - `authMiddleware` is an express middleware function to be inserted/used when a module's routes require a user to be authenticated to access them 25 | - `serverConfig` is the server's configuration at start up. If your module requires configuration at mount, it can be accessed here. 26 | 27 | This function is to return an object containing two items. The first is the context route this module will be mounted on (eg `/dev`). The second is an express [Router](https://expressjs.com/en/4x/api.html#router), which this module will have appended it's handlers to. 28 | 29 | This router will be invoked when enabled by the core express server, allowing the registered handlers on the router to then handle the request. 30 | 31 | ### Run time 32 | 33 | At runtime, before any module handlers are called, a piece of express middleware defined by the core module will run. The result of this middleware is as follows: 34 | 35 | - Create a context object in `res.locals` called `strimziuicontext`. This context will contain: 36 | - The current configuration for the server 37 | - A unique request ID 38 | - A pre configured set of loggers to use for the current module 39 | - Perform a check when receiving the request to see if the module is enabled, and thus should respond. If it is not enabled, the router for the module will not be invoked, and will try the next registered router. 40 | 41 | In the event two modules register a handler for the same route (E.g `/foo/bar`), the first module registered will have it's handler(s) called. 42 | -------------------------------------------------------------------------------- /server/core/core.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: core module 4 | 5 | Behaviours and capabilities provided by the core module 6 | 7 | Scenario: When making a call with no strimzi-ui header, one is added for later requests 8 | Given a 'mockapi_only' server configuration 9 | And I run an instance of the Strimzi-UI server 10 | When I make a request with no unique request header 11 | Then a unique request header is returned in the response 12 | 13 | Scenario: When making a call with a strimzi-ui header, that header is used in the request 14 | Given a 'mockapi_only' server configuration 15 | And I run an instance of the Strimzi-UI server 16 | When I make a request with a unique request header 17 | Then the unique request header sent is returned in the response 18 | 19 | Scenario: When making a call to the strimzi-ui server, the expected secuirty headers are present 20 | Given a 'mockapi_only' server configuration 21 | And I run an instance of the Strimzi-UI server 22 | When I make a 'get' request to '/api/test' 23 | Then all expected security headers are present 24 | 25 | Scenario: If two modules mount routes on the same mounting point, and one is disabled, the enabled module is invoked 26 | Given a 'mockapi_only' server configuration 27 | And I run an instance of the Strimzi-UI server 28 | When I make a 'get' request to '/api/test' 29 | Then the mockapi handler is called 30 | 31 | Scenario: When making a call to the strimzi-ui server, the expected session cookie is present 32 | Given a 'mockapi_only' server configuration 33 | And a session identifier of 'server-name' 34 | And I run an instance of the Strimzi-UI server 35 | When I make a 'get' request to '/' 36 | Then the response sets a cookie named 'server-name' -------------------------------------------------------------------------------- /server/core/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { returnExpress } from './app'; 6 | -------------------------------------------------------------------------------- /server/core/modules.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from 'api/index'; 6 | export * from 'client/index'; 7 | export * from 'config/index'; 8 | export * from 'log/index'; 9 | export * from 'mockapi/index'; 10 | -------------------------------------------------------------------------------- /server/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const merge = require('lodash.merge'); 6 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 7 | const { jestModuleMapper } = require('../utils/tooling/aliasHelper'); 8 | const { compilerOptions } = require('./tsconfig.json'); 9 | const commonConfig = require('../test_common/jest.common.config'); 10 | 11 | const config = { 12 | testMatch: ['/**/*.(spec|steps).[jt]s?(x)'], 13 | coverageDirectory: '/../coverage/server', 14 | testEnvironment: 'node', 15 | collectCoverageFrom: [ 16 | '/**/*.{js,ts,jsx,tsx}', 17 | '!**/index.{js,ts,jsx,tsx}', 18 | '!**/*.steps.*', 19 | '!**/*.d.ts', 20 | '!jest.config.js', 21 | '!**/test_common/**', 22 | '!*', 23 | ], 24 | moduleNameMapper: { 25 | ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 26 | ...jestModuleMapper, 27 | }, 28 | }; 29 | 30 | module.exports = merge({}, commonConfig, config); 31 | -------------------------------------------------------------------------------- /server/log/README.md: -------------------------------------------------------------------------------- 1 | # log 2 | 3 | This module is responsible for receiving and merging client logging events into the server's log 4 | via a WebSocket listener that is handled here. 5 | -------------------------------------------------------------------------------- /server/log/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { LogModule } from './router'; 6 | -------------------------------------------------------------------------------- /server/log/log.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: log module 4 | 5 | Behaviours and capabilities provided by the log module 6 | 7 | Scenario: Returns with the expected HTTP response for a /log call 8 | Given a 'log_only' server configuration 9 | And I run an instance of the Strimzi-UI server 10 | When I make a 'get' request to '/log' 11 | Then I get the expected log response 12 | 13 | Scenario: Sets up the WebSocket connection on /log call 14 | Given a 'log_only' server configuration 15 | And I run an instance of the Strimzi-UI server 16 | And I enable WebSocket connections on the Strimzi-UI server 17 | When I make a WebSocket connection request to '/log' 18 | And I send a logging WebSocket message 19 | And I send a logging WebSocket message without a clientLevel 20 | And I send a logging WebSocket message that is not a JSON array 21 | And I send an unparsable string logging WebSocket message 22 | And I send a non-string logging WebSocket message 23 | And I close the WebSocket 24 | Then the WebSocket has received 5 messages 25 | And the WebSocket is closed -------------------------------------------------------------------------------- /server/log/log.steps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { And, Then, Fusion } from 'jest-cucumber-fusion'; 6 | import { stepWithWorld } from 'test_common/commonServerSteps'; 7 | 8 | Then( 9 | 'I get the expected log response', 10 | stepWithWorld((world) => { 11 | const { request } = world; 12 | return request.expect(426); 13 | }) 14 | ); 15 | 16 | And( 17 | 'I send a logging WebSocket message', 18 | stepWithWorld(async (world) => { 19 | const { websocket } = world; 20 | 21 | websocket.send( 22 | JSON.stringify([ 23 | { 24 | clientLevel: 'warn', 25 | msg: 'test logging message', 26 | }, 27 | ]) 28 | ); 29 | }) 30 | ); 31 | 32 | And( 33 | 'I send a logging WebSocket message without a clientLevel', 34 | stepWithWorld(async (world) => { 35 | const { websocket } = world; 36 | websocket.send( 37 | JSON.stringify([ 38 | { 39 | msg: 'test logging message', 40 | }, 41 | ]) 42 | ); 43 | }) 44 | ); 45 | 46 | And( 47 | 'I send a non-string logging WebSocket message', 48 | stepWithWorld(async (world) => { 49 | const { websocket } = world; 50 | websocket.send(new ArrayBuffer(10)); 51 | }) 52 | ); 53 | 54 | And( 55 | 'I send a logging WebSocket message that is not a JSON array', 56 | stepWithWorld(async (world) => { 57 | const { websocket } = world; 58 | websocket.send( 59 | JSON.stringify({ 60 | clientLevel: 'warn', 61 | msg: 'test logging message', 62 | }) 63 | ); 64 | }) 65 | ); 66 | 67 | And( 68 | 'I send an unparsable string logging WebSocket message', 69 | stepWithWorld(async (world) => { 70 | const { websocket } = world; 71 | websocket.send('{this is not: "json"}'); 72 | }) 73 | ); 74 | 75 | Fusion('log.feature'); 76 | -------------------------------------------------------------------------------- /server/log/router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import express from 'express'; 6 | import { Logger } from 'pino'; 7 | import { Data } from 'ws'; 8 | import { 9 | ClientLoggingEvent, 10 | strimziUIRequestType, 11 | strimziUIResponseType, 12 | UIServerModule, 13 | } from 'types'; 14 | 15 | const moduleName = 'log'; 16 | 17 | export const LogModule: UIServerModule = { 18 | moduleName, 19 | addModule: (logger, authFn) => { 20 | const { exit } = logger.entry('addModule'); 21 | const routerForModule = express.Router(); 22 | 23 | // implementation to follow 24 | routerForModule.get('*', authFn, (req, res) => { 25 | const { isWs } = req as strimziUIRequestType; 26 | const { ws } = res as strimziUIResponseType; 27 | if (isWs) { 28 | ws.on('message', messageHandler(logger)); 29 | ws.on('close', closeHandler(logger)); 30 | } else { 31 | // Return 426 Upgrade Required if this isn't a websocket request 32 | res.sendStatus(426); 33 | } 34 | }); 35 | 36 | return exit({ mountPoint: '/log', routerForModule }); 37 | }, 38 | }; 39 | 40 | const messageHandler: (logger: Logger) => (data: Data) => void = (logger) => ( 41 | data 42 | ) => { 43 | if (typeof data === 'string') { 44 | try { 45 | JSON.parse(data).forEach((clientLogEvent: ClientLoggingEvent) => { 46 | if (clientLogEvent.clientLevel) { 47 | logger[clientLogEvent.clientLevel](clientLogEvent); 48 | } else { 49 | logger.debug(clientLogEvent); 50 | } 51 | }); 52 | } catch (err) { 53 | // Ignore any data that cannot be parsed 54 | logger.trace({ err }, `messageHandler failed: ${err.message}, ${data}`); 55 | } 56 | } else { 57 | // Ignore any non-string data 58 | logger.trace( 59 | `messageHandler ignoring data of type ${typeof data}: ${data}` 60 | ); 61 | } 62 | }; 63 | 64 | const closeHandler: ( 65 | logger: Logger 66 | ) => (code: number, reason: string) => void = (logger) => (code, reason) => 67 | logger.debug(`WebSocket listener closed. (${code}) ${reason}`); 68 | -------------------------------------------------------------------------------- /server/logging.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import pinoLogger from 'pino'; 7 | import pinoHttpLogger from 'pino-http'; 8 | import { randomBytes } from 'crypto'; 9 | 10 | import { getServerName } from 'serverConfig'; 11 | import { entryExitLoggerType } from 'types'; 12 | 13 | const STRIMZI_UI_REQUEST_ID_HEADER = 'x-strimzi-ui-request'; 14 | 15 | const serverName = getServerName(); 16 | 17 | let rootLogger = pinoLogger(); 18 | 19 | /** Replaces the rootLogger with a new pinoLogger with the supplied options */ 20 | const updateRootLoggerOptions: ( 21 | loggerOptions: pinoLogger.LoggerOptions, 22 | isUpdate: boolean 23 | ) => void = (loggerOptions, isUpdate) => { 24 | rootLogger.info( 25 | { 26 | serverName, 27 | module: 'logging', 28 | loggerOptions, 29 | }, 30 | `Strimzi ui logging options${isUpdate ? ' updated' : ''}` 31 | ); 32 | try { 33 | // Update the root logger with the new options 34 | rootLogger = pinoLogger(loggerOptions); 35 | } catch (err) { 36 | rootLogger.error( 37 | { 38 | serverName, 39 | module: 'logging', 40 | loggerOptions, 41 | err, 42 | }, 43 | `Failed to configure logging options: ${err.message}` 44 | ); 45 | } 46 | }; 47 | 48 | /** Generate an entryExitLogger */ 49 | const generateLogger: ( 50 | module: string, 51 | requestID?: string 52 | ) => entryExitLoggerType = (module, requestID) => { 53 | const logger = rootLogger.child({ 54 | serverName, 55 | module, 56 | requestID, 57 | }); 58 | 59 | const entryExitLogger = { 60 | ...logger, 61 | entry: (fnName, ...params) => { 62 | logger.trace({ fnName, params }, 'entry'); 63 | return { 64 | exit: (returns) => { 65 | logger.trace({ fnName, returns }, 'exit'); 66 | return returns; // return params so you can do `return exit(.....)` 67 | }, 68 | }; 69 | }, 70 | }; 71 | 72 | // Wrap the logging functions (trace(), debug(), info(), warn(), error(), and fatal()) in functions 73 | // so that the entryExitLogger can be destructured. e.g: const {info} = res.locals.strimziuicontext.logger 74 | Object.keys(logger.levels.values).forEach((level) => { 75 | entryExitLogger[level] = (msg, ...args) => logger[level](msg, ...args); 76 | }); 77 | 78 | return entryExitLogger as entryExitLoggerType; 79 | }; 80 | 81 | /** Generate a pino-http HttpLogger */ 82 | const generateHttpLogger: () => pinoHttpLogger.HttpLogger = () => 83 | pinoHttpLogger({ 84 | logger: rootLogger, 85 | useLevel: 'debug', 86 | genReqId: generateRequestID, 87 | reqCustomProps: () => ({ 88 | serverName, 89 | module: 'core', 90 | }), 91 | }); 92 | 93 | /** Generate or retrieve the request ID used in the pino-http HttpLogger */ 94 | const generateRequestID: (Request) => string = (req) => { 95 | if (!req.headers[STRIMZI_UI_REQUEST_ID_HEADER]) { 96 | req.headers[STRIMZI_UI_REQUEST_ID_HEADER] = randomBytes(8).toString('hex'); 97 | } 98 | return req.headers[STRIMZI_UI_REQUEST_ID_HEADER] as string; 99 | }; 100 | 101 | export { 102 | generateLogger, 103 | generateHttpLogger, 104 | updateRootLoggerOptions, 105 | STRIMZI_UI_REQUEST_ID_HEADER, 106 | }; 107 | -------------------------------------------------------------------------------- /server/mockapi/README.md: -------------------------------------------------------------------------------- 1 | # mockapi 2 | 3 | This module is used at development and test time to emulate a real instance of `Strimzi-admin`. It will return mock data as if a real instance of `Strimzi-admin` had responded, and also offer additional mutations to trigger responses needed in particular scenarios (eg user not authorised to perform an action etc). 4 | 5 | _Note_: Implementation to follow in a future PR 6 | -------------------------------------------------------------------------------- /server/mockapi/data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | // placeholder GQL schema for a topic/topic list - ideally to come from file 7 | export const schema = 8 | 'type Topic {name: String partitions: Int replicas: Int } type Query { topic(name: String): Topic topics: [Topic] } '; 9 | -------------------------------------------------------------------------------- /server/mockapi/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export { MockApiModule } from './router'; 6 | -------------------------------------------------------------------------------- /server/mockapi/mockapi.feature: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | Feature: mockapi module 4 | 5 | Behaviours and capabilities provided by the mockapi module 6 | 7 | Scenario: Returns with the expected response for a mocked api call 8 | Given a 'mockapi_only' server configuration 9 | And I run an instance of the Strimzi-UI server 10 | When I make a 'mockTopicsQuery' gql request to '/api' 11 | Then I get the expected mockapi response 12 | 13 | Scenario: Returns with the expected response for a call to the test endpoint 14 | Given a 'mockapi_only' server configuration 15 | And I run an instance of the Strimzi-UI server 16 | When I make a 'get' request to '/api/test' 17 | Then I get the expected mockapi test endpoint response -------------------------------------------------------------------------------- /server/mockapi/mockapi.steps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { Then, Fusion } from 'jest-cucumber-fusion'; 6 | import { stepWithWorld } from 'test_common/commonServerSteps'; 7 | 8 | Then( 9 | 'I get the expected mockapi response', 10 | stepWithWorld((world) => { 11 | const { request } = world; 12 | return request.expect(200).expect((res) => { 13 | const { data } = res.body; 14 | 15 | expect(data).not.toBeUndefined(); 16 | 17 | const { topics } = data; 18 | 19 | expect(topics).not.toBeUndefined(); 20 | 21 | expect(Array.isArray(topics)).toBe(true); 22 | expect(topics.length).not.toBeLessThan(1); 23 | 24 | const { name, partitions, replicas } = topics[0]; 25 | 26 | expect(name).not.toBeUndefined(); 27 | expect(partitions).not.toBeUndefined(); 28 | expect(replicas).not.toBeUndefined(); 29 | }); 30 | }) 31 | ); 32 | 33 | Then( 34 | 'I get the expected mockapi test endpoint response', 35 | stepWithWorld((world) => { 36 | const { request } = world; 37 | return request.expect(418); 38 | }) 39 | ); 40 | 41 | Fusion('mockapi.feature'); 42 | -------------------------------------------------------------------------------- /server/mockapi/router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import express from 'express'; 6 | import { UIServerModule } from 'types'; 7 | import bodyParser from 'body-parser'; 8 | import { ApolloServer } from 'apollo-server-express'; 9 | import { schema } from './data'; 10 | 11 | const moduleName = 'mockapi'; 12 | 13 | export const MockApiModule: UIServerModule = { 14 | moduleName, 15 | addModule: (logger) => { 16 | const { exit } = logger.entry('addModule'); 17 | const routerForModule = express.Router(); 18 | 19 | // endpoint used for test purposes 20 | routerForModule.get('/test', (_, res) => { 21 | const { entry } = res.locals.strimziuicontext.logger; 22 | const { exit } = entry('`/test` handler'); 23 | res.setHeader('x-strimzi-ui-module', moduleName); 24 | res.sendStatus(418); 25 | exit(418); 26 | }); 27 | 28 | const server = new ApolloServer({ 29 | typeDefs: schema, 30 | resolvers: {}, 31 | debug: true, 32 | mockEntireSchema: true, 33 | }); 34 | 35 | routerForModule.use(bodyParser.json(), server.getMiddleware({ path: '/' })); 36 | 37 | return exit({ mountPoint: '/api', routerForModule }); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /server/placeholderFunctionsToReplace.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | // placeholder functions - to be replaced by actual implementation later 6 | 7 | import express from 'express'; 8 | import { authenticationConfigType } from 'types'; 9 | 10 | // https://github.com/orgs/strimzi/projects/2#card-44265081 11 | // function which returns a piece of express middleware for a given auth strategy 12 | const authFunction: ( 13 | config: authenticationConfigType 14 | ) => ( 15 | req: express.Request, 16 | res: express.Response, 17 | next: express.NextFunction 18 | ) => void = ({ strategy }) => { 19 | switch (strategy) { 20 | default: 21 | case 'none': 22 | return (req, res, next) => next(); 23 | case 'scram': 24 | case 'oauth': 25 | return (req, res) => res.sendStatus(511); // if auth on, reject for sake of example. This is a middleware, akin to passport doing its checks. 26 | } 27 | }; 28 | 29 | export { authFunction }; 30 | -------------------------------------------------------------------------------- /server/serverConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | import { existsSync, watch } from 'fs'; 7 | import { resolve } from 'path'; 8 | import merge from 'lodash.merge'; 9 | import { server } from 'ui-config/index'; 10 | 11 | const { defaultConfig, serverConfigPath, serverName } = server.values; 12 | 13 | import { serverConfigType, loggerType } from 'types'; 14 | 15 | /** Out of the box when built by webpack, it replaces `require` with it's own version (`__webpack_require__`), which requires static paths. As we use require to load a config from an envvar, we need the node require function (`__non_webpack_require__`, as called by webpack). Thus, check if we are in a webpack built environment (I.e `__non_webpack_require__` is defined), and if so, use it, else use `require` (which will be the normal node require, used via ts-node etc) */ 16 | /* eslint-disable no-undef */ 17 | const requireForConfigLoad = 18 | typeof __non_webpack_require__ !== 'undefined' && __non_webpack_require__ 19 | ? __non_webpack_require__ 20 | : require; 21 | /* eslint-enable no-undef */ 22 | 23 | const defaultServerConfig = (defaultConfig as unknown) as serverConfigType; 24 | 25 | export const getDefaultConfig: () => serverConfigType = () => 26 | merge({}, defaultServerConfig); 27 | 28 | const pathToConfigFile = resolve(serverConfigPath as string); 29 | 30 | const configFileExists = existsSync(pathToConfigFile); 31 | export const getServerName: () => string = () => serverName as string; 32 | 33 | export const loadConfig: ( 34 | callback: (config: serverConfigType) => void, 35 | logger: loggerType 36 | ) => void = (callback, logger) => { 37 | let config = merge({}, defaultServerConfig); 38 | 39 | if (configFileExists) { 40 | logger.info(`Using config file '${pathToConfigFile}'`); 41 | 42 | delete requireForConfigLoad.cache[ 43 | requireForConfigLoad.resolve(pathToConfigFile) 44 | ]; 45 | // this is a deliberate require so we can load json/js, and have it parsed/modules evaluated 46 | const loadedConfig = requireForConfigLoad(pathToConfigFile); 47 | // merge parsed with core/std config 48 | config = merge(config, loadedConfig); 49 | } else { 50 | logger.error( 51 | `The file specified config file '${pathToConfigFile}' was not found. Will fallback to default config` 52 | ); 53 | } 54 | callback(config); 55 | }; 56 | 57 | export const watchConfig: ( 58 | callbackOnConfigChange: (newConfig: serverConfigType) => void, 59 | logger: loggerType 60 | ) => void = (callbackOnConfigChange, logger) => 61 | configFileExists && 62 | watch( 63 | pathToConfigFile, 64 | undefined, 65 | (evt) => evt === 'change' && loadConfig(callbackOnConfigChange, logger) 66 | ); 67 | -------------------------------------------------------------------------------- /server/test_common/__test_fixtures__/README.md: -------------------------------------------------------------------------------- 1 | # server test fixtures 2 | 3 | The files in this directory are used in test cases/configuration. 4 | 5 | - `client` - mock public files, used in the `client` module tests 6 | - `main.http.conf.js` and `main.https.conf.js` - config files used by the `main.feature` testing the server when running on node with real files and security settings 7 | -------------------------------------------------------------------------------- /server/test_common/__test_fixtures__/client/images/picture.svg: -------------------------------------------------------------------------------- 1 | 5 | picture file -------------------------------------------------------------------------------- /server/test_common/__test_fixtures__/client/index.html: -------------------------------------------------------------------------------- 1 | 5 | index file {{bootstrapConfigs}} 6 | -------------------------------------------------------------------------------- /server/test_common/__test_fixtures__/client/protected.html: -------------------------------------------------------------------------------- 1 | 5 | protected file 6 | -------------------------------------------------------------------------------- /server/test_common/__test_fixtures__/main.http.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | module.exports = { 6 | modules: { 7 | api: false, 8 | client: false, 9 | config: true, 10 | mockapi: true, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /server/test_common/__test_fixtures__/main.https.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import * as config from './main.http.conf'; 6 | 7 | import { serverCertificates } from '../../../utils/tooling/runtimeDevUtils.js'; 8 | 9 | module.exports = { 10 | ...config, 11 | client: { 12 | transport: { 13 | ...serverCertificates, 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /server/test_common/testConfigs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { resolve } from 'path'; 6 | import { serverConfigType } from 'types'; 7 | import { getDefaultConfig } from 'serverConfig'; 8 | import merge from 'lodash.merge'; 9 | 10 | const defaultConfig: () => serverConfigType = () => getDefaultConfig(); 11 | 12 | const defaultTestConfig: () => serverConfigType = () => 13 | merge(defaultConfig(), { logging: {} }); 14 | 15 | const modules = { 16 | api: false, 17 | client: false, 18 | config: false, 19 | log: false, 20 | mockapi: false, 21 | }; 22 | 23 | const mockapiModuleConfig: () => serverConfigType = () => 24 | merge({}, defaultTestConfig(), { 25 | modules: { ...modules, mockapi: true }, 26 | }); 27 | 28 | const logModuleConfig: () => serverConfigType = () => 29 | merge({}, defaultTestConfig(), { 30 | modules: { ...modules, log: true }, 31 | }); 32 | 33 | const configModuleConfig: () => serverConfigType = () => 34 | merge({}, defaultTestConfig(), { 35 | modules: { ...modules, config: true }, 36 | }); 37 | 38 | const configModuleWithConfigOverrides: () => serverConfigType = () => 39 | merge({}, configModuleConfig(), { 40 | client: { 41 | configOverrides: { 42 | version: '34.0.0', 43 | }, 44 | }, 45 | featureFlags: { 46 | client: { 47 | Home: { 48 | showVersion: false, 49 | }, 50 | }, 51 | testFlag: true, 52 | }, 53 | }); 54 | 55 | const clientModuleConfig: () => serverConfigType = () => 56 | merge({}, defaultTestConfig(), { 57 | client: { 58 | publicDir: resolve(__dirname, './__test_fixtures__/client'), 59 | }, 60 | modules: { ...modules, client: true }, 61 | }); 62 | 63 | const apiModuleConfig: () => serverConfigType = () => 64 | merge({}, defaultTestConfig(), { 65 | proxy: { 66 | hostname: 'test-backend', 67 | port: 3434, 68 | }, 69 | modules: { ...modules, api: true }, 70 | }); 71 | 72 | const apiModuleConfigWithCustomContextRoot: () => serverConfigType = () => 73 | merge({}, defaultTestConfig(), { 74 | proxy: { 75 | hostname: 'test-backend', 76 | port: 3434, 77 | contextRoot: '/myCustomContextRoot', 78 | }, 79 | modules: { ...modules, api: true }, 80 | }); 81 | 82 | const securedApiModuleConfig: () => serverConfigType = () => 83 | merge(apiModuleConfig(), { 84 | proxy: { 85 | transport: { 86 | cert: 'mock certificate', 87 | }, 88 | }, 89 | }); 90 | 91 | export const getConfigForName: (name: string) => serverConfigType = (name) => { 92 | switch (name) { 93 | default: 94 | case 'default': 95 | case 'production': 96 | return defaultTestConfig(); 97 | case 'mockapi_only': 98 | return mockapiModuleConfig(); 99 | case 'log_only': 100 | return logModuleConfig(); 101 | case 'config_only': 102 | return configModuleConfig(); 103 | case 'config_only_with_config_overrides': 104 | return configModuleWithConfigOverrides(); 105 | case 'client_only': 106 | return clientModuleConfig(); 107 | case 'api_only': 108 | return apiModuleConfig(); 109 | case 'api_secured_only': 110 | return securedApiModuleConfig(); 111 | case 'api_with_custom_context_root': 112 | return apiModuleConfigWithCustomContextRoot(); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /server/test_common/testGQLRequests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | /** 7 | * Mock requests used to test the gql elements of the server. Query names referenced in feature files, E.g `When I make a 'mockTopicsQuery' gql request to ...`. These can be generated using GraphiQL and it's export feature 8 | */ 9 | const mockTopicsQuery = { 10 | query: '{\n topics {\n name\n partitions\n replicas\n }\n}\n', 11 | }; 12 | 13 | const getConfigAndFeatureFlagQuery = { 14 | query: 15 | '\n{\n featureFlags {\n _generatedTypeName\n }\n client {\n _generatedTypeName\n }\n server {\n _generatedTypeName\n }\n}', 16 | }; 17 | 18 | const getConfigAndFeatureFlagQueryWithConfigOverrides = { 19 | query: 20 | '{\n server {\n _generatedTypeName\n }\n featureFlags {\n client {\n Home {\n showVersion\n }\n }\n testFlag\n }\n\tclient {\n version\n }\n}\n', 21 | }; 22 | 23 | export const requests = { 24 | mockTopicsQuery, 25 | getConfigAndFeatureFlagQuery, 26 | getConfigAndFeatureFlagQueryWithConfigOverrides, 27 | }; 28 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "../js/server", 6 | "baseUrl": "./", 7 | "paths": { 8 | "ui-config/*": ["../config/*"] 9 | } 10 | }, 11 | "include": ["."] 12 | } 13 | -------------------------------------------------------------------------------- /test_common/README.md: -------------------------------------------------------------------------------- 1 | # Test Common 2 | 3 | This directory contains all files that provide common utility and configuration to all tests. See the full [test utilities doc here](../docs/Test.md#test-utilities). The implementation of the utilities can be found [here.](../utils/test) 4 | 5 | ## Contents 6 | 7 | - `jest_cucumber_support` - common cucumber steps and code 8 | - `jest.config.js` - jest configuration 9 | - `mockfile.util.ts` - file used by tests to stub in binary assets referenced/used across the repo 10 | - `test.babel.js` - babel configuration used in tests 11 | - `tsconfig.test.json` - TypeScript configuration for test cases 12 | -------------------------------------------------------------------------------- /test_common/jest.common.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const { jestModuleMapper } = require('../utils/tooling/aliasHelper'); 6 | const { resolve } = require('path'); 7 | const config = { 8 | rootDir: '.', 9 | clearMocks: true, 10 | testTimeout: 10000, // required for server tests, which take ~3 seconds to start 11 | setupFiles: [resolve(__dirname, 'jest_cucumber_support/index.ts')], 12 | preset: 'ts-jest/presets/js-with-ts', 13 | moduleNameMapper: { 14 | ...jestModuleMapper, 15 | }, 16 | coverageReporters: ['json', 'text', 'lcov', 'json-summary'], 17 | moduleDirectories: ['node_modules', ''], 18 | coverageThreshold: { 19 | global: { 20 | branches: 100, 21 | functions: 100, 22 | lines: 100, 23 | statements: 100, 24 | }, 25 | }, 26 | }; 27 | 28 | module.exports = config; 29 | -------------------------------------------------------------------------------- /test_common/jest_cucumber_support/README.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | This folder is used by `jest-cucumber-fusion` as a bootstrap for all tests. Common step definitions for all tests should be added to `common_stepdefs.ts`. 4 | -------------------------------------------------------------------------------- /test_common/jest_cucumber_support/commonStepdefs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export const common_stepdefs = {}; 6 | -------------------------------------------------------------------------------- /test_common/jest_cucumber_support/commonTestTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export interface genericWorldType { 6 | /** area for tests to put test specific information which need passing between steps */ 7 | context: Record; 8 | } 9 | 10 | interface withWorldInterface { 11 | (callback: (world: T, ...others: Array) => T): ( 12 | ...others: Array 13 | ) => void; 14 | (callback: (world: T, ...others: Array) => void): ( 15 | ...others: Array 16 | ) => void; 17 | } 18 | 19 | /** generates a world for use in cucumber tests. The world must extend genericWorldType. The world value provided will be used as the staring value/reset to via the returned resetWorld() function */ 20 | export const worldGenerator: ( 21 | world: T 22 | ) => { 23 | /** function to restore the world to a starting state. Call this before each test */ 24 | resetWorld: () => void; 25 | /* wraps your callback with one which adds world access. It will invoke your callback passing the world in argument one, and then any others following. This function allows access the world, but also to update it. Your step's callback should return the world once complete so the new world object can be used by downstream steps */ 26 | stepWhichUpdatesWorld: withWorldInterface; 27 | /** wraps your callback with one which adds world access. It will invoke your callback passing the world in argument one, and then any others following. */ 28 | stepWithWorld: withWorldInterface; 29 | } = (world) => { 30 | let worldInstance = world; 31 | 32 | //Force async so that callbacks will always be handled as promises in Jest 33 | return { 34 | resetWorld: () => { 35 | worldInstance = { ...world }; 36 | }, 37 | stepWhichUpdatesWorld: (callback) => async (...stepArguments) => 38 | (worldInstance = await callback(worldInstance, ...stepArguments)), 39 | stepWithWorld: (callback) => async (...stepArguments) => 40 | callback(worldInstance, ...stepArguments), 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /test_common/jest_cucumber_support/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | /* eslint-disable */ 6 | import { common_stepdefs } from './commonStepdefs'; 7 | import * as commonServerStepDefinitions from '../../server/test_common/commonServerSteps'; 8 | -------------------------------------------------------------------------------- /test_common/jest_rtl_setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | // add custom jest matchers from jest-dom 6 | import '@testing-library/jest-dom'; 7 | -------------------------------------------------------------------------------- /test_common/mockfile.util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export default 'test-file-stub'; 6 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "noImplicitAny": false, 15 | "outDir": "./js" 16 | }, 17 | "exclude": ["utils", "build", "**/test_common", "**/*.js"] 18 | } 19 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | This directory contains common helper code or configuration for a variety of purposes across the repo. Where applicable, common areas of help code are split out into their own directories, with their own READMEs. Consult these READMEs for more details about the code available, and their usage. 4 | 5 | ## Utilities available 6 | 7 | ### dev_config 8 | 9 | Configuration used for development purposes. See the [README](./dev_config/README.md) for further details. 10 | 11 | ### tooling 12 | 13 | - `constants.js` - file containing development/build time constants. 14 | - `aliasHelper.js` - logic used to generate code aliases. 15 | 16 | ### Test 17 | 18 | Test helper/common code. See the [README](./test/README.md) for further details. 19 | -------------------------------------------------------------------------------- /utils/dev_config/README.md: -------------------------------------------------------------------------------- 1 | # dev_config 2 | 3 | This directory contains helper code and configuration used when developing the Strimzi UI. It is not intended to be shipped. Files include: 4 | 5 | - [`mockadmin.config.js`](./mockadmin.config.js) - configuration for the mock admin server 6 | - [`server.dev.config.js`](./server.dev.config.js) - configuration for the server used during development 7 | - [`server.e2e.config.js`](./server.e2e.config.js) - configuration for the server used during e2e tests 8 | - `req.conf` - `openssl` configuration for generated certificates. Used by [`generateCerts.sh`](../tooling/generateCerts.sh). 9 | -------------------------------------------------------------------------------- /utils/dev_config/mockadmin.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const { 6 | mockAdminCertificates, 7 | devEnvValues, 8 | } = require('../tooling/runtimeDevUtils.js'); 9 | const { mockadminServer } = devEnvValues; 10 | 11 | module.exports = { 12 | authentication: { 13 | strategy: 'none', 14 | }, 15 | client: { 16 | transport: { 17 | ...mockAdminCertificates, 18 | }, 19 | }, 20 | logging: { 21 | level: 'debug', 22 | prettyPrint: { 23 | translateTime: true, 24 | }, 25 | }, 26 | modules: { 27 | api: false, 28 | client: false, 29 | config: true, 30 | mockapi: true, 31 | }, 32 | proxy: { 33 | transport: {}, 34 | }, 35 | ...mockadminServer, 36 | }; 37 | -------------------------------------------------------------------------------- /utils/dev_config/req.conf: -------------------------------------------------------------------------------- 1 | # Copyright Strimzi authors. 2 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 3 | # This file is a config for local SSL certificate generation, used to enable development/test of the UI server in a SSL enabled server 4 | 5 | [req] 6 | default_bits = 2048 # explicitly set cert size to 2048 bits - most modern browsers reject anything shorter 7 | distinguished_name = req_distinguished_name 8 | x509_extensions = req_ext # x509 key for self-signed certs 9 | prompt = no 10 | 11 | # Set meta data for certificate 12 | [req_distinguished_name] 13 | C = UK 14 | ST = Test 15 | L = Test 16 | O = Test 17 | OU = Test 18 | CN = localhost 19 | 20 | # set list of altnames for the certificate 21 | [req_ext] 22 | subjectAltName = @alt_names 23 | 24 | # add localhost to alt_names so express server tests will accept the certificate 25 | [alt_names] 26 | DNS.1 = localhost 27 | DNS.2 = 127.0.0.1 28 | -------------------------------------------------------------------------------- /utils/dev_config/server.dev.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const { 6 | serverCertificates, 7 | mockAdminCertificates, 8 | devEnvValues, 9 | } = require('../tooling/runtimeDevUtils.js'); 10 | const { devServer, mockadminServer } = devEnvValues; 11 | 12 | module.exports = { 13 | client: { 14 | transport: { 15 | ...serverCertificates, 16 | }, 17 | }, 18 | logging: { 19 | level: 'debug', 20 | prettyPrint: { 21 | translateTime: true, 22 | }, 23 | }, 24 | modules: { 25 | api: true, 26 | client: false, 27 | config: true, 28 | log: true, 29 | mockapi: false, 30 | }, 31 | proxy: { 32 | ...mockadminServer, 33 | transport: { 34 | ...mockAdminCertificates, 35 | }, 36 | }, 37 | ...devServer, 38 | }; 39 | -------------------------------------------------------------------------------- /utils/dev_config/server.e2e.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const { 6 | serverCertificates, 7 | mockAdminCertificates, 8 | devEnvValues, 9 | } = require('../tooling/runtimeDevUtils.js'); 10 | const { devServer, mockadminServer } = devEnvValues; 11 | 12 | // used in e2e, this is the UI server running as it would in a prod environment, but using mock admin/certificate config 13 | module.exports = { 14 | client: { 15 | transport: { 16 | ...serverCertificates, 17 | }, 18 | }, 19 | logging: { 20 | level: 'debug', 21 | prettyPrint: { 22 | translateTime: true, 23 | }, 24 | }, 25 | proxy: { 26 | ...mockadminServer, 27 | transport: { 28 | ...mockAdminCertificates, 29 | }, 30 | }, 31 | ...devServer, 32 | }; 33 | -------------------------------------------------------------------------------- /utils/test/README.md: -------------------------------------------------------------------------------- 1 | # Test utilities 2 | 3 | Common helper code used under test. See individual directories for details of the helpers. All are expected to be publicly exported via the `index.ts` barrel file in this directory, so test code can reference them as follows: 4 | 5 | ``` 6 | ... 7 | import { renderWithContextProviders } from 'utils/test'; 8 | ... 9 | ``` 10 | 11 | ## Provided utilities 12 | 13 | - [Context](./context/README.md) - utilities for writing tests for components that use contexts. 14 | - [i18n](./i18n/README.md) - i18n test helpers 15 | - [withApollo](./withApollo/README.md) - Apollo client helper utilities 16 | -------------------------------------------------------------------------------- /utils/test/context/README.md: -------------------------------------------------------------------------------- 1 | # Testing contexts 2 | 3 | Testing contexts is an example of having common test logic that can be 4 | abstracted away. Here, the function `renderWithContextProviders` has been 5 | provided that extends the base rtl `render` function but can be given a list 6 | of context providers and their values that will wrap the provided children. 7 | Using this, a common render can be defined in a test file for contexts so that 8 | each test only needs to worry about using a consumer. For example: 9 | 10 | ```typescript 11 | render = (children) => 12 | renderWithContextProviders(children, {}, [ 13 | { provider: MyProvider1, value: providerValue1 }, 14 | { provider: MyProvider2, value: providerValue2 }, 15 | ]); 16 | ``` 17 | 18 | This can be defined in a test file so that tests can still use a `render` 19 | function in the same way they usually would with rtl but these renders will be 20 | wrapped in the providers defined in the above function. 21 | 22 | ## Available functions 23 | 24 | - `renderWithContextProviders` - described above 25 | - `renderWithConfigFeatureFlagContext` - uses `renderWithContextProviders` to render given JSX with a default `ConfigFeatureFlag` context provided 26 | - `renderWithCustomConfigFeatureFlagContext` - same as `renderWithConfigFeatureFlagContext`, but allows a specific value for the `ConfigFeatureFlag` context to be provided 27 | -------------------------------------------------------------------------------- /utils/test/context/context.util.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import React, { Provider, ReactElement, ReactNode } from 'react'; 6 | import { render, RenderResult } from '@testing-library/react'; 7 | import { 8 | ConfigFeatureFlag, 9 | defaultClientConfig, 10 | defaultConfigFeatureFlagValue, 11 | } from 'Contexts'; 12 | import { ApolloQueryResponseType } from 'Contexts/types'; 13 | 14 | type TestProviderWithValue = { 15 | value: T; 16 | provider: Provider; 17 | }; 18 | 19 | const contextWrapper: ( 20 | providers: Array>, 21 | children: ReactNode 22 | ) => JSX.Element = (providers = [], children = null) => ( 23 | 24 | {providers.reduceRight( 25 | (accJsx, { provider: ProviderToMount, value }) => ( 26 | {accJsx} 27 | ), 28 | children 29 | )} 30 | 31 | ); 32 | 33 | const renderWithContextProviders: ( 34 | ui: ReactElement, 35 | options: Record, 36 | providers: Array> 37 | ) => RenderResult = (ui, options, providers) => 38 | render(ui, { 39 | wrapper: ({ children }) => { 40 | return contextWrapper(providers, children); 41 | }, 42 | ...options, 43 | }); 44 | 45 | /** renderWithConfigFeatureFlagContext renders the given `ui` JSX with a ConfigFeatureFlag provider seeded with default values. Use this if your components indirectly use/require a value from the ConfigFeatureFlag context, else use `renderWithCustomConfigFeatureFlagContext` so the value from ConfigFeatureFlag can be controlled */ 46 | const renderWithConfigFeatureFlagContext: ( 47 | ui: ReactElement, 48 | options?: Record 49 | ) => RenderResult = (ui, options = {}) => 50 | renderWithContextProviders(ui, options, [ 51 | { 52 | provider: ConfigFeatureFlag.Provider, 53 | value: defaultConfigFeatureFlagValue, 54 | }, 55 | ]); 56 | 57 | /** renderWithCustomConfigFeatureFlagContext renders the given `ui` JSX with a ConfigFeatureFlag provider provided via `configFeatureFlagValue`. */ 58 | const renderWithCustomConfigFeatureFlagContext: ( 59 | configFeatureFlagValue: ApolloQueryResponseType, 60 | ui: ReactElement, 61 | options?: Record 62 | ) => RenderResult = ( 63 | configFeatureFlagValue = defaultClientConfig, 64 | ui, 65 | options = {} 66 | ) => 67 | renderWithContextProviders(ui, options, [ 68 | { 69 | provider: ConfigFeatureFlag.Provider, 70 | value: { ...defaultConfigFeatureFlagValue, ...configFeatureFlagValue }, 71 | }, 72 | ]); 73 | 74 | export { 75 | renderWithContextProviders, 76 | renderWithConfigFeatureFlagContext, 77 | renderWithCustomConfigFeatureFlagContext, 78 | }; 79 | -------------------------------------------------------------------------------- /utils/test/i18n/README.md: -------------------------------------------------------------------------------- 1 | # Testing i18n 2 | 3 | When testing React components, it can be desirable to check for the presence of a string in the output. However, for test purposes we do not care about the actual string (we trust that `react-i18next` library works.) 4 | 5 | Instead, we just need to assert that the correct translation key, formatting elements and inserts are used. 6 | 7 | This module exports two functions: 8 | 9 | `translate` - that takes in two parameters `key` and `inserts` - and returns a string with the following format: 10 | 11 | ``` 12 | key:"$key" inserts:{"key": "value"} 13 | ``` 14 | 15 | `translateWithFormatting` - that takes in two parameters `key` and `children` - where children can be one or many React Nodes or key:value pairs. It returns a string with the following format: 16 | 17 | ``` 18 | key:"$key" elements:[...] inserts:[{"key": "value"}] 19 | ``` 20 | 21 | These can then be used by any `react-i18next` mock in tests to create a fake translation. It can also be used in RTL tests to assert that the expected text exists in the document. 22 | -------------------------------------------------------------------------------- /utils/test/i18n/i18n.util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | import { ReactNode, ReactElement, ReactNodeArray } from 'react'; 6 | 7 | const stringify: (node: ReactNode) => string = (node) => { 8 | if (!node) { 9 | return ''; 10 | } 11 | 12 | if ((node as ReactElement).props) { 13 | const props = Object.entries((node as ReactElement).props) 14 | .filter(([key]) => key !== 'children') 15 | .reduce( 16 | (currentProps, [prop, value]) => 17 | `${currentProps} ${prop}=${JSON.stringify(value)}`, 18 | '' 19 | ); 20 | 21 | return `"<${(node as ReactElement).type} ${props.trim()}/>"`; 22 | } 23 | 24 | return node.toString(); 25 | }; 26 | 27 | /** 28 | * Very primitive check - react-i18n inserts objects are single key/value 29 | * @param node 30 | */ 31 | const isCustomElement: (node: ReactNode) => boolean = (node) => { 32 | if (typeof node !== 'object' || !node) { 33 | return false; 34 | } 35 | 36 | if (Array.isArray(node)) { 37 | return false; 38 | } 39 | 40 | //Mini workaround - inserts objects in `react-i18n` must have a single key 41 | return Object.keys(node).length > 1; 42 | }; 43 | 44 | export const translate: ( 45 | key: string, 46 | options?: Record 47 | ) => string = (key, insertsObj) => { 48 | let inserts = ''; 49 | 50 | if (insertsObj) { 51 | inserts = JSON.stringify( 52 | insertsObj.inserts ? insertsObj.inserts : insertsObj 53 | ); 54 | } 55 | 56 | return `key:"${key}"${inserts ? ` inserts:${inserts}` : ''}`; 57 | }; 58 | 59 | export const translateWithFormatting: ( 60 | key: string, 61 | children: ReactNode 62 | ) => string = (key, children) => { 63 | const elements: Array = []; 64 | const inserts: Array = []; 65 | 66 | const processChild: (child: ReactNode) => void = (child) => { 67 | if ((child as ReactNodeArray).length) { 68 | return; 69 | } 70 | 71 | if (isCustomElement(child)) { 72 | elements.push(stringify(child)); 73 | } else { 74 | inserts.push(JSON.stringify(child)); 75 | } 76 | }; 77 | 78 | if (children) { 79 | //Handle top level array only 80 | if ((children as ReactNodeArray).length) { 81 | (children as ReactNodeArray).forEach(processChild); 82 | } else { 83 | processChild(children); 84 | } 85 | } 86 | 87 | return `key:"${key}"${elements.length ? ` elements:[${elements}]` : ''}${ 88 | inserts.length ? ` inserts:[${inserts}]` : '' 89 | }`; 90 | }; 91 | -------------------------------------------------------------------------------- /utils/test/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | export * from './context/context.util'; 6 | export * from './i18n/i18n.util'; 7 | export * from './withApollo/withApollo.util'; 8 | -------------------------------------------------------------------------------- /utils/test/withApollo/README.md: -------------------------------------------------------------------------------- 1 | # withApollo 2 | 3 | A set of helper utilities to simplify testing with apollo client calls 4 | 5 | - `withApolloProviderReturning` - for use in client tests. Wraps child JSX with a `MockedProvider`, returning a provided result. Example usage: 6 | 7 | ``` 8 | const { getByText } = render(withApolloProviderReturning(myResponseForTestCase, )); 9 | ``` 10 | 11 | - `apolloMockResponse` - for use in client tests. Skips one process tick, allowing requests to return/re render. Example usage: 12 | 13 | ``` 14 | ... 15 | // trigger the query 16 | userEvent.click(getByText('Search')); 17 | await apolloMockResponse(); 18 | expect(getByText('Foo')); 19 | .... 20 | ``` 21 | 22 | - `generateMockResponseForGQLRequest` - for use in tests/storybook to simulate the response from an Apollo call. Encapsulates the structure expected by the Apollo MockProvider 23 | - `generateMockDataResponseForGQLRequest` and `generateMockErrorResponseForGQLRequest` - uses `generateMockResponseForGQLRequest` to return responses to provide to the Apollo MockProvider for success/failure conditions when making a request in tests/storybook scenarios 24 | -------------------------------------------------------------------------------- /utils/tooling/README.md: -------------------------------------------------------------------------------- 1 | # Tooling 2 | 3 | This directory contains any code that is used for development coding. For example, code that aids linting or the Webpack build process. 4 | 5 | ## Contents 6 | 7 | - [headers](./headers/README.md) - header to be inserted at the top of every file. 8 | - `constants.js` - constants used in the build process/at develop time 9 | - `aliasHelper.js` - generates aliases for webpack/jest for modules in the UI 10 | - `runtimeDevUtils.js` - helper code used by development server configuration/webpack dev serer 11 | - `generateCerts.sh` - bash script which via `openssl` creates development certificates to be used by the mock and strimzi ui server in development 12 | -------------------------------------------------------------------------------- /utils/tooling/aliasHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | 6 | const ignoredBinaries = ['png', 'svg', 'ico', 'scss'].join('|'); 7 | const testCommon = '/../test_common'; 8 | const mockFile = `${testCommon}/mockfile.util.ts`; 9 | 10 | const jestModuleMapper = { 11 | [`^.+\\.(${ignoredBinaries})$`]: mockFile, 12 | }; 13 | 14 | module.exports = { 15 | jestModuleMapper, 16 | }; 17 | -------------------------------------------------------------------------------- /utils/tooling/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const headerTextDir = path.resolve(__dirname, './headers'); 9 | const headerTextContent = fs 10 | .readdirSync(headerTextDir) // read all files in the headers directory 11 | .filter((file) => file.includes('.txt')) // remove any which are not .txt 12 | .map((file) => `${headerTextDir}/${file}`) // construct file path 13 | .reduce((acc, fileToRead) => { 14 | // reduce content to a single string by 15 | const rawHeaderText = fs.readFileSync(fileToRead, 'utf-8'); // reading the current file 16 | return `${acc} * ${rawHeaderText.split('\n').join(`\n * `)}\n *\n`; // insert it's content into the header text, breaking on new lines 17 | }, ''); 18 | 19 | // /*! required so not stripped in build - wrap header content with start/end tags 20 | const HEADER_TEXT = `/*!\n${headerTextContent} */`; 21 | 22 | module.exports = { 23 | HEADER_TEXT, // used by build to insert into built content 24 | PRODUCTION: 'production', 25 | DEVELOPMENT: 'development', 26 | }; 27 | -------------------------------------------------------------------------------- /utils/tooling/generateCerts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Strimzi authors. 3 | # License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | # generate 2 pairs of certificates and private keys for development purposes. Certificates valid for 1 year. Any valid certificate/key pair called `strimzi-ui-server` or `strimzi-ui-mock-admin` could be used for development, in present in the `generated/dev_certs/` directory 5 | mkdir -p generated/dev_certs/ 6 | openssl req -days 365 -nodes -new -x509 -keyout generated/dev_certs/strimzi-ui-server.key -out generated/dev_certs/strimzi-ui-server.cert -config utils/dev_config/req.conf 7 | openssl req -days 365 -nodes -new -x509 -keyout generated/dev_certs/strimzi-ui-mock-admin.key -out generated/dev_certs/strimzi-ui-mock-admin.cert -config utils/dev_config/req.conf -------------------------------------------------------------------------------- /utils/tooling/headers/README.md: -------------------------------------------------------------------------------- 1 | # Headers 2 | 3 | This directory contains files with copyright/header statements which will be programmatically added to all source files. These headers are applied at build time to all built output, and at lint time via the `npm run lint:allfiles` script. Files in this folder should: 4 | 5 | - Be named sensibly (ie StrimziHeader contains the Strimzi header text) 6 | - Be `.txt` files so [`constants.js`](../constants.js) can discover and include all headers in built output. 7 | -------------------------------------------------------------------------------- /utils/tooling/headers/StrimziHeader.txt: -------------------------------------------------------------------------------- 1 | Copyright Strimzi authors. 2 | License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). -------------------------------------------------------------------------------- /utils/tooling/runtimeDevUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Strimzi authors. 3 | * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). 4 | */ 5 | const { existsSync, readFileSync } = require('fs'); 6 | const { resolve } = require('path'); 7 | 8 | // development time environment variable overrides. 9 | const devEnvValues = { 10 | // webpack dev server hostname and port 11 | webpackDevServer: { 12 | hostname: process.env.WDS_HOSTNAME || 'localhost', 13 | port: process.env.WDS_PORT || 8080, 14 | }, 15 | // mock admin server hostname, port and api module context root 16 | mockadminServer: { 17 | hostname: process.env.MA_HOSTNAME || 'localhost', 18 | port: process.env.MA_PORT || 9080, 19 | contextRoot: process.env.MA_CONTEXT_ROOT || '/api', 20 | }, 21 | // (development instance) server hostname and port 22 | devServer: { 23 | hostname: process.env.SD_HOSTNAME || 'localhost', 24 | port: process.env.SD_PORT || 3000, 25 | }, 26 | }; 27 | 28 | const getIfExists = (file) => 29 | existsSync(resolve(__dirname, file)) 30 | ? readFileSync(resolve(__dirname, file)) 31 | : undefined; 32 | 33 | const serverCertificates = { 34 | cert: getIfExists('../../generated/dev_certs/strimzi-ui-server.cert'), 35 | key: getIfExists('../../generated/dev_certs/strimzi-ui-server.key'), 36 | }; 37 | 38 | const mockAdminCertificates = { 39 | cert: getIfExists('../../generated/dev_certs/strimzi-ui-mock-admin.cert'), 40 | key: getIfExists('../../generated/dev_certs/strimzi-ui-mock-admin.key'), 41 | }; 42 | 43 | module.exports = { 44 | devEnvToUseTls: 45 | serverCertificates.cert && serverCertificates.key ? true : false, 46 | serverCertificates, 47 | mockAdminCertificates, 48 | devEnvValues, 49 | }; 50 | --------------------------------------------------------------------------------