├── .babelrc ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── cron-release.yml │ ├── cypress.yml │ ├── pr-checks.yml │ └── stale.yml ├── .gitignore ├── .ls-lint.yml ├── .prettierignore ├── .prettierrc.yaml ├── .stylelintrc.json ├── .yamllint ├── CHANGES.md ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── build.config.js ├── shared.config.js └── start.config.js ├── cypress.config.js ├── cypress.env.json ├── cypress ├── e2e │ └── smoke.js └── support │ └── e2e.js ├── docs └── index.md ├── eslint.config.mjs ├── lingui.config.js ├── locale ├── en.js ├── en.po ├── es.js ├── es.po ├── fr.js ├── fr.po ├── ja.js ├── ja.po ├── ko.js ├── ko.po ├── nl.js ├── nl.po ├── zh.js └── zh.po ├── package-lock.json ├── package.json ├── pulp-ui-config.json ├── src ├── actions │ ├── action.tsx │ ├── ansible-remote-create.tsx │ ├── ansible-remote-delete.tsx │ ├── ansible-remote-download-ca.tsx │ ├── ansible-remote-download-client.tsx │ ├── ansible-remote-download-requirements.tsx │ ├── ansible-remote-edit.tsx │ ├── ansible-repository-collection-version-add.tsx │ ├── ansible-repository-collection-version-remove.tsx │ ├── ansible-repository-copy.tsx │ ├── ansible-repository-create.tsx │ ├── ansible-repository-delete.tsx │ ├── ansible-repository-edit.tsx │ ├── ansible-repository-sync.tsx │ ├── ansible-repository-version-revert.tsx │ ├── file-remote-create.tsx │ ├── file-remote-delete.tsx │ ├── file-remote-edit.tsx │ ├── file-repository-create.tsx │ ├── file-repository-delete.tsx │ ├── file-repository-edit.tsx │ ├── file-repository-sync.tsx │ └── index.ts ├── api │ ├── activities.ts │ ├── ansible-distribution.ts │ ├── ansible-remote.ts │ ├── ansible-repository.ts │ ├── base.ts │ ├── certificate-upload.ts │ ├── collection-version.ts │ ├── collection.ts │ ├── container-distribution.ts │ ├── container-tag.ts │ ├── execution-environment-namespace.ts │ ├── execution-environment-registry.ts │ ├── execution-environment-remote.ts │ ├── execution-environment.ts │ ├── file-content.ts │ ├── file-distribution.ts │ ├── file-publication.ts │ ├── file-remote.ts │ ├── file-repository.ts │ ├── generic-pulp.ts │ ├── get-cancel-token.ts │ ├── group-role.ts │ ├── group.ts │ ├── import.ts │ ├── index.ts │ ├── namespace.ts │ ├── orphan-cleanup.ts │ ├── pulp-login.ts │ ├── pulp-status.ts │ ├── pulp.ts │ ├── repair.ts │ ├── response-types │ │ ├── collection.ts │ │ ├── execution-environment.ts │ │ ├── import.ts │ │ ├── namespace.ts │ │ ├── permissions.ts │ │ ├── pulp.ts │ │ ├── remote.ts │ │ ├── role.ts │ │ ├── task.ts │ │ └── user.ts │ ├── role.ts │ ├── rpm-package.ts │ ├── rpm-repository.ts │ ├── sign-collections.ts │ ├── sign-containers.ts │ ├── signing-service.ts │ ├── tag.ts │ ├── task-purge.ts │ ├── task.ts │ └── user.ts ├── app-context.tsx ├── app-routes.tsx ├── app.scss ├── components │ ├── access-tab.tsx │ ├── alert-list.tsx │ ├── applied-filters.tsx │ ├── approval-row.tsx │ ├── approve-modal.tsx │ ├── base-header.tsx │ ├── breadcrumbs.tsx │ ├── card-list-switcher.tsx │ ├── cards.scss │ ├── collection-card.tsx │ ├── collection-content-list.scss │ ├── collection-content-list.tsx │ ├── collection-dependencies-list.tsx │ ├── collection-dropdown.tsx │ ├── collection-filter.ts │ ├── collection-header.tsx │ ├── collection-info.scss │ ├── collection-info.tsx │ ├── collection-list-item.tsx │ ├── collection-list.tsx │ ├── collection-usedby-dependencies-list.tsx │ ├── compound-filter.tsx │ ├── confirm-modal.tsx │ ├── container-repository-form.tsx │ ├── copy-collection-to-repository-modal.tsx │ ├── copy-url.tsx │ ├── darkmode-switcher.tsx │ ├── data-form.tsx │ ├── date-component.tsx │ ├── delete-collection-modal.tsx │ ├── delete-execution-environment-modal.tsx │ ├── delete-group-modal.tsx │ ├── delete-modal.tsx │ ├── delete-remote-modal.tsx │ ├── delete-repository-modal.tsx │ ├── delete-user-modal.tsx │ ├── deprecated-tag.tsx │ ├── detail-list.tsx │ ├── details.tsx │ ├── download-count.tsx │ ├── download-signature-grid-item.tsx │ ├── empty-state-custom.tsx │ ├── empty-state-filter.tsx │ ├── empty-state-no-data.tsx │ ├── empty-state-unauthorized.tsx │ ├── empty-state-under-construction.tsx │ ├── empty-state-xs.tsx │ ├── error-boundary.tsx │ ├── execution-environment-header.tsx │ ├── external-link.tsx │ ├── form-field-helper.tsx │ ├── group-modal.tsx │ ├── header.scss │ ├── help-button.tsx │ ├── import-console.tsx │ ├── import-list.tsx │ ├── import-modal.scss │ ├── import-modal.tsx │ ├── index.ts │ ├── language-switcher.tsx │ ├── lazy-distributions.tsx │ ├── lazy-repositories.tsx │ ├── link-tabs.tsx │ ├── list-item-actions.tsx │ ├── list-item.scss │ ├── list-page.tsx │ ├── loading-page.tsx │ ├── loading-spinner.tsx │ ├── login-link.tsx │ ├── logo.tsx │ ├── main.tsx │ ├── markdown-editor.scss │ ├── markdown-editor.tsx │ ├── maybe-link.tsx │ ├── multi-repo-modal.tsx │ ├── multi-search-search.tsx │ ├── multiple-repo-selector.tsx │ ├── my-imports.scss │ ├── namespace-card.tsx │ ├── namespace-form.scss │ ├── namespace-form.tsx │ ├── namespace-list-item.tsx │ ├── namespace-modal.tsx │ ├── not-found.scss │ ├── not-found.tsx │ ├── numeric-label.tsx │ ├── orphan-cleanup-task-modal.tsx │ ├── page-with-tabs.tsx │ ├── page.tsx │ ├── partner-header.tsx │ ├── patternfly-wrappers │ │ ├── l10n.tsx │ │ └── tooltip.tsx │ ├── permission-categories.tsx │ ├── permission-chip-selector.tsx │ ├── preview-roles.tsx │ ├── pulp-about-modal.tsx │ ├── pulp-code-block.tsx │ ├── pulp-copy-button.tsx │ ├── pulp-download-button.tsx │ ├── pulp-labels.tsx │ ├── pulp-list-toolbar.tsx │ ├── pulp-pagination.tsx │ ├── purge-task-modal.tsx │ ├── remote-form.tsx │ ├── repair-task-modal.tsx │ ├── repository-badge.tsx │ ├── repository-form.tsx │ ├── resources-form.tsx │ ├── role-form.tsx │ ├── role-header.tsx │ ├── role-list-table.tsx │ ├── role-permissions.tsx │ ├── select-group.tsx │ ├── select-roles.tsx │ ├── select-user.tsx │ ├── sha-label.tsx │ ├── sign-all-certificates-modal.tsx │ ├── sign-single-certificate-modal.tsx │ ├── signature-badge.tsx │ ├── small-logo.tsx │ ├── sort-table.scss │ ├── sort-table.tsx │ ├── sort.tsx │ ├── stateful-dropdown.tsx │ ├── status-indicator.tsx │ ├── switcher.scss │ ├── table-of-contents.tsx │ ├── tag-label.tsx │ ├── tag-manifest-modal.tsx │ ├── tag.tsx │ ├── typeahead.tsx │ ├── ui-version.tsx │ ├── upload-signature-modal.tsx │ ├── user-form-page.tsx │ ├── user-form.tsx │ └── wizard-modal.tsx ├── containers │ ├── about-project.tsx │ ├── ansible-remote │ │ ├── detail.tsx │ │ ├── edit.tsx │ │ ├── list.tsx │ │ ├── tab-access.tsx │ │ └── tab-details.tsx │ ├── ansible-repository │ │ ├── detail.tsx │ │ ├── edit.tsx │ │ ├── list.tsx │ │ ├── tab-access.tsx │ │ ├── tab-collection-versions.tsx │ │ ├── tab-details.tsx │ │ ├── tab-distributions.tsx │ │ └── tab-repository-versions.tsx │ ├── approvals │ │ ├── approvals.scss │ │ └── approvals.tsx │ ├── collection-detail │ │ ├── base.ts │ │ ├── collection-content.tsx │ │ ├── collection-dependencies.scss │ │ ├── collection-dependencies.tsx │ │ ├── collection-detail.scss │ │ ├── collection-detail.tsx │ │ ├── collection-distributions.tsx │ │ ├── collection-docs.tsx │ │ └── collection-import-log.tsx │ ├── edit-namespace │ │ └── edit-namespace.tsx │ ├── execution-environment-detail │ │ ├── base.tsx │ │ ├── execution-environment-detail-access.tsx │ │ ├── execution-environment-detail-activities.tsx │ │ ├── execution-environment-detail-images.scss │ │ ├── execution-environment-detail-images.tsx │ │ ├── execution-environment-detail.scss │ │ └── execution-environment-detail.tsx │ ├── execution-environment-list │ │ ├── execution-environment-list.tsx │ │ └── execution-environment.scss │ ├── execution-environment-manifest │ │ ├── execution-environment-manifest.scss │ │ └── execution-environment-manifest.tsx │ ├── execution-environment │ │ └── registry-list.tsx │ ├── file-remote │ │ ├── detail.tsx │ │ ├── edit.tsx │ │ ├── list.tsx │ │ └── tab-details.tsx │ ├── file-repository │ │ ├── detail.tsx │ │ ├── edit.tsx │ │ ├── list.tsx │ │ ├── tab-details.tsx │ │ ├── tab-distributions.tsx │ │ └── tab-repository-versions.tsx │ ├── group-management │ │ ├── group-detail-role-management │ │ │ ├── group-detail-role-management.scss │ │ │ └── group-detail-role-management.tsx │ │ ├── group-detail.tsx │ │ └── group-list.tsx │ ├── index.ts │ ├── login │ │ └── login.tsx │ ├── my-imports │ │ └── my-imports.tsx │ ├── namespace-detail │ │ └── namespace-detail.tsx │ ├── namespace-list │ │ ├── my-namespaces.tsx │ │ ├── namespace-list.scss │ │ ├── namespace-list.tsx │ │ └── partners.tsx │ ├── pulp-status.tsx │ ├── role-management │ │ ├── role-create.tsx │ │ ├── role-edit.tsx │ │ └── role-list.tsx │ ├── rpm │ │ └── package-list.tsx │ ├── search │ │ ├── multi-search.tsx │ │ ├── search.scss │ │ └── search.tsx │ ├── settings │ │ └── user-profile.tsx │ ├── signature-keys │ │ └── list.tsx │ ├── task-management │ │ ├── task-detail.tsx │ │ ├── task-list-view.tsx │ │ └── task.scss │ └── user-management │ │ ├── user-create.tsx │ │ ├── user-detail.tsx │ │ ├── user-edit.tsx │ │ └── user-list.tsx ├── darkmode.ts ├── entrypoint.tsx ├── index.d.ts ├── index.html ├── l10n.ts ├── layout.tsx ├── menu.tsx ├── paths.ts ├── routes │ └── root.tsx ├── ui-config.ts ├── user-context.tsx └── utilities │ ├── assign-roles.ts │ ├── can-sign.ts │ ├── content-summary.ts │ ├── delete-collection.ts │ ├── download-data.ts │ ├── fail-alerts.tsx │ ├── filter-is-set.ts │ ├── get-human-size.ts │ ├── get-repo-url.ts │ ├── index.ts │ ├── last-sync-task.tsx │ ├── login-url.ts │ ├── map-error-messages.ts │ ├── map-role-errors.ts │ ├── model-to-api.ts │ ├── param-helper.ts │ ├── parse-pulp-id.ts │ ├── plugin-repository-base-path.ts │ ├── plugin-version.ts │ ├── repo-signing.ts │ ├── repository-distro.ts │ ├── repository-remove-collection.ts │ ├── task-alert.tsx │ ├── translate-locked-role.ts │ ├── translate-task.ts │ ├── truncate-sha.ts │ ├── validate-url-helper.ts │ ├── wait-for-task.ts │ └── with-router.tsx ├── static └── images │ ├── favicon.ico │ ├── pulp_logo.png │ └── pulp_logo_icon.svg └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | ["@babel/preset-react", { "runtime": "automatic" }], 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [ 8 | "@babel/plugin-transform-runtime", 9 | "@lingui/babel-plugin-lingui-macro" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # don't look up 2 | root = true 3 | 4 | # unix newlines, final newline, UTF-8, and 2 spaces for indent 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | # markdown must allow trailing spaces 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | # Makefiles can't use spaces 18 | [Makefile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | groups: 8 | babel: 9 | patterns: 10 | - '@babel/*' 11 | - 'babel-loader' 12 | eslint: 13 | patterns: 14 | - 'eslint' 15 | - 'eslint-config-*' 16 | - 'eslint-plugin-*' 17 | - 'typescript-eslint' 18 | lingui: 19 | patterns: 20 | - '@lingui/*' 21 | patternfly: 22 | patterns: 23 | - '@patternfly/*' 24 | react: 25 | patterns: 26 | - 'react' 27 | - 'react-dom' 28 | ignore: 29 | - dependency-name: '@patternfly/*' 30 | update-types: 31 | - 'version-update:semver-major' 32 | 33 | - package-ecosystem: 'github-actions' 34 | directory: '/' 35 | schedule: 36 | interval: 'weekly' 37 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: 'PR checks' 2 | 3 | on: 4 | pull_request: 5 | branches: ['main'] 6 | 7 | jobs: 8 | pr-checks: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout pulp-ui (${{ github.ref }})' 12 | uses: actions/checkout@v4 13 | 14 | - name: 'Install node 20' 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: '20' 18 | cache: 'npm' 19 | 20 | - name: 'Install python 3.13' 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.13' 24 | 25 | - name: 'Checks' 26 | run: | 27 | # fail if npm install had to change package-lock.json 28 | npm install 29 | git diff --exit-code package-lock.json 30 | 31 | # dependencies 32 | npm run lint-setup 33 | 34 | # run linters 35 | npm run lint 36 | 37 | # uses `this` but not `class` 38 | sudo apt install -y ripgrep 39 | rg '\bclass\b' src/ | cut -d: -f1 | sort -u > src.class 40 | rg '\bthis[\.,)\]}]\b' src/ | cut -d: -f1 | sort -u > src.this 41 | if [ `comm -13 src.class src.this | wc -l` -ne 0 ]; then 42 | echo 43 | echo "Files using this but not class:" 44 | echo 45 | comm -13 src.class src.this 46 | echo 47 | rg '\bthis[\.,)\]}]\b' `comm -13 src.class src.this` 48 | echo 49 | exit 1 50 | fi 51 | 52 | merge-commits: 53 | runs-on: ubuntu-latest 54 | steps: 55 | # need to checkout out head, the merge commit won't have any merges in history 56 | # also need more than 1 commit, assuming no PR will have more than 128 57 | - name: 'Checkout pulp-ui HEAD' 58 | uses: actions/checkout@v4 59 | with: 60 | fetch-depth: 128 61 | ref: ${{ github.event.pull_request.head.sha }} 62 | 63 | - name: 'Ensure no merge commits' 64 | run: | 65 | # fail on merge commits in the PR 66 | # since squash&merge doesn't create merge commits, 67 | # and the last non-squash merges were in Jul 2019, 68 | # we can just look for any merge commits since 2020 69 | count=`git log --min-parents=2 --since 2020-01-01 | tee /dev/stderr | wc -l` 70 | [ "$count" = 0 ] 71 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '4 4 * * 4' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write # only for delete-branch option 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | days-before-issue-stale: 384 17 | days-before-pr-stale: 192 18 | days-before-close: 32 19 | stale-issue-label: 'stale' 20 | stale-pr-label: 'stale' 21 | exempt-issue-labels: 'pinned' 22 | exempt-pr-labels: 'pinned' 23 | delete-branch: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw[po] 2 | .DS_Store 3 | .idea/ 4 | .vscode/ 5 | coverage/ 6 | cypress/downloads/* 7 | cypress/fixtures/example.json 8 | cypress/screenshots/* 9 | cypress/videos/* 10 | dist/ 11 | node_modules/ 12 | npm-debug.log* 13 | -------------------------------------------------------------------------------- /.ls-lint.yml: -------------------------------------------------------------------------------- 1 | ls: 2 | .config.js: point.case 3 | .d.ts: kebab-case 4 | .js: kebab-case 5 | .jsx: kebab-case 6 | .ts: kebab-case 7 | .tsx: kebab-case 8 | 9 | ignore: 10 | - '.git' 11 | - 'dist' 12 | - 'node_modules' 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.mp4 3 | *.png 4 | *.po 5 | *.sh 6 | *.xml 7 | .*.sw[po] 8 | .DS_Store 9 | cypress/fixtures/ 10 | locale/ 11 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | jsxSingleQuote: true 3 | trailingComma: 'all' 4 | bracketSameLine: false 5 | importOrder: 6 | - '^[.]/app.scss' 7 | - '' 8 | - '^src/' 9 | - '^static/' 10 | - '^[./]' 11 | importOrderSortSpecifiers: true 12 | plugins: 13 | - '@trivago/prettier-plugin-sort-imports' 14 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-recommended-scss", 3 | "rules": { 4 | "custom-property-pattern": "^([a-z][a-z0-9]*)(-+[a-zA-Z0-9_]+)*$", 5 | "selector-class-pattern": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | ignore: | 2 | /node_modules/ 3 | 4 | extends: default 5 | 6 | rules: 7 | line-length: 8 | max: 240 9 | indentation: 10 | indent-sequences: consistent 11 | brackets: 12 | max-spaces-inside: 1 13 | document-start: disable 14 | truthy: 15 | check-keys: false 16 | comments: 17 | min-spaces-from-content: 1 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # WARNING 2 | # This Dockerfile is intended for development purposes only. Do not use it for production deployments 3 | # Copied from https://github.com/ansible/ansible-hub-ui/blob/master/Dockerfile 4 | 5 | FROM node:20-alpine 6 | WORKDIR /pulp/ 7 | 8 | RUN mkdir -p /pulp/app/ && \ 9 | apk add --no-cache git && \ 10 | git config --global --add safe.directory /pulp/app 11 | 12 | # install npm in /pulp and mount the app in /pulp/app so that the installed node_modules 13 | # doesn't trample node_modules on your computer. see https://www.docker.com/blog/keep-nodejs-rockin-in-docker/ for details 14 | COPY package.json package-lock.json /pulp/ 15 | RUN npm install 16 | 17 | # make webpack-dev-sever and other node packages executable 18 | ENV PATH /pulp/node_modules/.bin:$PATH 19 | 20 | WORKDIR /pulp/app 21 | EXPOSE 8002 22 | CMD ["npm", "run", "start"] 23 | -------------------------------------------------------------------------------- /config/build.config.js: -------------------------------------------------------------------------------- 1 | const webpackBase = require('./shared.config'); 2 | 3 | module.exports = webpackBase({ 4 | WEBPACK_PUBLIC_PATH: '/static/pulp_ui/', 5 | }); 6 | -------------------------------------------------------------------------------- /config/start.config.js: -------------------------------------------------------------------------------- 1 | const webpackBase = require('./shared.config'); 2 | 3 | const proxyTarget = process.env.API_PROXY || 'http://localhost:8080'; 4 | 5 | module.exports = webpackBase({ 6 | // Port that the UI is served over. 7 | DEV_PORT: 8002, 8 | 9 | // Serve the UI over http or https. Options: true, false 10 | DEV_HTTPS: false, 11 | 12 | // Value for webpack.devServer.proxy 13 | // https://webpack.js.org/configuration/dev-server/#devserverproxy 14 | // used to get around CORS requirements when running in dev mode 15 | DEV_PROXY: { 16 | '/api/': proxyTarget, 17 | '/assets/': proxyTarget, 18 | '/auth/': proxyTarget, 19 | '/extensions/': proxyTarget, 20 | '/pulp/': proxyTarget, 21 | '/static/rest_framework/': proxyTarget, 22 | '/v2/': proxyTarget, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | // https://on.cypress.io/configuration 2 | const { defineConfig } = require('cypress'); 3 | 4 | module.exports = defineConfig({ 5 | e2e: { 6 | baseUrl: 'http://localhost:8002', 7 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 8 | }, 9 | video: !!process.env.RUNNER_DEBUG, // only record videos when running github action in debug mode 10 | viewportHeight: 1080, 11 | viewportWidth: 1920, 12 | }); 13 | -------------------------------------------------------------------------------- /cypress.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "API_BASE_PATH": "/pulp/api/v3/", 3 | "UI_BASE_PATH": "/ui/", 4 | "username": "admin", 5 | "password": "admin" 6 | } 7 | -------------------------------------------------------------------------------- /cypress/e2e/smoke.js: -------------------------------------------------------------------------------- 1 | describe('UI smoke tests', () => { 2 | beforeEach(() => cy.login()); 3 | 4 | it('Logout + login', () => { 5 | cy.ui(); 6 | 7 | // log out via top nav 8 | cy.get('[data-cy=user-dropdown]').click(); 9 | cy.contains('a', 'Logout').click(); 10 | 11 | // go to login via left nav 12 | cy.get('[data-cy=pulp-menu-item-Login]').click(); 13 | cy.assertTitle('Login'); 14 | 15 | // fill in form manually, submit 16 | cy.get('#pf-login-username-id').type(Cypress.env('username')); 17 | cy.get('#pf-login-password-id').type(Cypress.env('password')); 18 | cy.contains('button', 'Log in').click(); 19 | 20 | // check on Status, logged in 21 | cy.assertTitle('Status'); 22 | cy.get('[data-cy=user-dropdown]'); 23 | }); 24 | 25 | it('Navigation', () => { 26 | cy.ui(); 27 | 28 | // TODO 29 | }); 30 | 31 | it('Status', () => { 32 | cy.ui(); 33 | cy.assertTitle('Status'); 34 | 35 | cy.contains('Online workers'); 36 | }); 37 | 38 | it('Ansible repositories', () => { 39 | cy.ui('ansible/repositories'); 40 | cy.assertTitle('Repositories'); 41 | 42 | // TODO 43 | }); 44 | 45 | it('Ansible remotes', () => { 46 | cy.ui('ansible/remotes'); 47 | cy.assertTitle('Remotes'); 48 | 49 | // TODO 50 | }); 51 | 52 | it('File repositories', () => { 53 | cy.ui('file/repositories'); 54 | cy.assertTitle('Repositories'); 55 | 56 | // TODO 57 | }); 58 | 59 | it('File remotes', () => { 60 | cy.ui('file/remotes'); 61 | cy.assertTitle('Remotes'); 62 | 63 | // TODO 64 | }); 65 | 66 | it('RPMs', () => { 67 | cy.ui('rpm/rpms'); 68 | cy.assertTitle('Packages'); 69 | 70 | cy.contains('No packages yet'); 71 | }); 72 | 73 | it('Task management', () => { 74 | cy.ui('tasks'); 75 | cy.assertTitle('Task management'); 76 | 77 | // TODO 78 | }); 79 | 80 | it('Users', () => { 81 | cy.ui('users'); 82 | cy.assertTitle('Users'); 83 | 84 | // TODO 85 | }); 86 | 87 | it('Groups', () => { 88 | cy.ui('groups'); 89 | cy.assertTitle('Groups'); 90 | 91 | // TODO 92 | }); 93 | 94 | it('Roles', () => { 95 | cy.ui('roles'); 96 | cy.assertTitle('Roles'); 97 | 98 | // TODO 99 | }); 100 | 101 | it('About project', () => { 102 | cy.ui('about'); 103 | cy.assertTitle('About project'); 104 | 105 | // TODO 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // https://on.cypress.io/custom-commands 2 | 3 | // const api = Cypress.env('API_BASE_PATH'); 4 | const ui = Cypress.env('UI_BASE_PATH'); 5 | 6 | Cypress.Commands.add('assertTitle', {}, (title) => { 7 | cy.contains('.pf-v5-c-title', title); 8 | }); 9 | 10 | Cypress.Commands.add('ui', {}, (path = '') => { 11 | cy.visit(ui + path); 12 | }); 13 | 14 | Cypress.Commands.add('login', {}, (username, password) => { 15 | if (!username && !password) { 16 | // default to admin 17 | username = Cypress.env('username'); 18 | password = Cypress.env('password'); 19 | } 20 | 21 | cy.session(username, () => { 22 | window.sessionStorage.credentials = JSON.stringify({ 23 | username, 24 | password, 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /lingui.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@lingui/cli'; 2 | import { formatter } from '@lingui/format-po'; 3 | 4 | export default defineConfig({ 5 | catalogs: [ 6 | { 7 | path: '/locale/{locale}', 8 | include: ['/src'], 9 | }, 10 | ], 11 | format: formatter({ lineNumbers: false }), 12 | locales: ['en', 'es', 'fr', 'ko', 'nl', 'ja', 'zh'], 13 | sourceLocale: 'en', 14 | }); 15 | -------------------------------------------------------------------------------- /pulp-ui-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "API_BASE_PATH": "/pulp/api/v3/", 3 | "UI_BASE_PATH": "/ui/", 4 | "UI_EXTERNAL_LOGIN_URI": null, 5 | "EXTRA_VERSION": "" 6 | } 7 | -------------------------------------------------------------------------------- /src/actions/ansible-remote-create.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const ansibleRemoteCreateAction = Action({ 6 | title: msg`Add remote`, 7 | onClick: (item, { navigate }) => 8 | navigate(formatPath(Paths.ansible.remote.edit, { name: '_' })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/ansible-remote-delete.tsx: -------------------------------------------------------------------------------- 1 | import { msg, t } from '@lingui/core/macro'; 2 | import { AnsibleRemoteAPI } from 'src/api'; 3 | import { DeleteRemoteModal } from 'src/components'; 4 | import { 5 | handleHttpError, 6 | parsePulpIDFromURL, 7 | taskAlert, 8 | waitForTaskUrl, 9 | } from 'src/utilities'; 10 | import { Action } from './action'; 11 | 12 | export const ansibleRemoteDeleteAction = Action({ 13 | title: msg`Delete`, 14 | modal: ({ addAlert, listQuery, setState, state }) => 15 | state.deleteModalOpen ? ( 16 | setState({ deleteModalOpen: null })} 18 | deleteAction={() => 19 | deleteRemote(state.deleteModalOpen, { addAlert, setState, listQuery }) 20 | } 21 | name={state.deleteModalOpen.name} 22 | /> 23 | ) : null, 24 | onClick: ( 25 | { name, id, pulp_href }: { name: string; id?: string; pulp_href?: string }, 26 | { setState }, 27 | ) => 28 | setState({ 29 | deleteModalOpen: { pulpId: id || parsePulpIDFromURL(pulp_href), name }, 30 | }), 31 | }); 32 | 33 | function deleteRemote({ name, pulpId }, { addAlert, setState, listQuery }) { 34 | return AnsibleRemoteAPI.delete(pulpId) 35 | .then(({ data }) => { 36 | addAlert(taskAlert(data.task, t`Removal started for remote ${name}`)); 37 | setState({ deleteModalOpen: null }); 38 | return waitForTaskUrl(data.task); 39 | }) 40 | .then(() => listQuery()) 41 | .catch( 42 | handleHttpError(t`Failed to remove remote ${name}`, () => null, addAlert), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/actions/ansible-remote-download-ca.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { downloadString } from 'src/utilities'; 3 | import { Action } from './action'; 4 | 5 | export const ansibleRemoteDownloadCAAction = Action({ 6 | title: msg`Download CA certificate`, 7 | onClick: ({ ca_cert }) => downloadString(ca_cert, 'ca_cert'), 8 | visible: ({ ca_cert }) => !!ca_cert, 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/ansible-remote-download-client.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { downloadString } from 'src/utilities'; 3 | import { Action } from './action'; 4 | 5 | export const ansibleRemoteDownloadClientAction = Action({ 6 | title: msg`Download client certificate`, 7 | onClick: ({ client_cert }) => downloadString(client_cert, 'client_cert'), 8 | visible: ({ client_cert }) => !!client_cert, 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/ansible-remote-download-requirements.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { downloadString } from 'src/utilities'; 3 | import { Action } from './action'; 4 | 5 | export const ansibleRemoteDownloadRequirementsAction = Action({ 6 | title: msg`Download requirements YAML`, 7 | onClick: ({ requirements_file }) => 8 | downloadString(requirements_file, 'requirements.yml'), 9 | visible: ({ requirements_file }) => !!requirements_file, 10 | }); 11 | -------------------------------------------------------------------------------- /src/actions/ansible-remote-edit.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const ansibleRemoteEditAction = Action({ 6 | title: msg`Edit`, 7 | onClick: ({ name }, { navigate }) => 8 | navigate(formatPath(Paths.ansible.remote.edit, { name })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/ansible-repository-copy.tsx: -------------------------------------------------------------------------------- 1 | import { msg, t } from '@lingui/core/macro'; 2 | import { AnsibleDistributionAPI } from 'src/api'; 3 | import { getDistroURL, repositoryDistro } from 'src/utilities'; 4 | import { Action } from './action'; 5 | 6 | export const ansibleRepositoryCopyAction = Action({ 7 | title: msg`Copy CLI configuration`, 8 | onClick: async (item, { addAlert }) => { 9 | let distro = null; 10 | 11 | if (!item.distro) { 12 | addAlert({ 13 | id: 'copy-cli-config', 14 | title: t`Loading distribution...`, 15 | variant: 'info', 16 | }); 17 | 18 | distro = await repositoryDistro( 19 | item.name, 20 | item.pulp_href, 21 | AnsibleDistributionAPI, 22 | ).catch(() => null); 23 | } else { 24 | distro = item.distro; 25 | } 26 | 27 | if (!distro) { 28 | addAlert({ 29 | id: 'copy-cli-config', 30 | title: t`There are no distributions associated with this repository.`, 31 | variant: 'danger', 32 | }); 33 | return; 34 | } 35 | 36 | const cliConfig = [ 37 | '[galaxy]', 38 | `server_list = ${distro.name}`, 39 | '', 40 | `[galaxy_server.${distro.name}]`, 41 | `url=${getDistroURL(distro)}`, 42 | 'token=', 43 | ].join('\n'); 44 | 45 | navigator.clipboard.writeText(cliConfig); 46 | addAlert({ 47 | description:
{cliConfig}
, 48 | id: 'copy-cli-config', 49 | title: t`Successfully copied to clipboard`, 50 | variant: 'success', 51 | }); 52 | }, 53 | disabled: (item) => { 54 | // disabled check only available on detail screen 55 | if ('distro' in item && !item.distro) { 56 | return t`There are no distributions associated with this repository.`; 57 | } 58 | 59 | return null; 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /src/actions/ansible-repository-create.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const ansibleRepositoryCreateAction = Action({ 6 | title: msg`Add repository`, 7 | onClick: (item, { navigate }) => 8 | navigate(formatPath(Paths.ansible.repository.edit, { name: '_' })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/ansible-repository-edit.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const ansibleRepositoryEditAction = Action({ 6 | title: msg`Edit`, 7 | onClick: ({ name }, { navigate }) => 8 | navigate(formatPath(Paths.ansible.repository.edit, { name })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/file-remote-create.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const fileRemoteCreateAction = Action({ 6 | title: msg`Add remote`, 7 | onClick: (item, { navigate }) => 8 | navigate(formatPath(Paths.file.remote.edit, { name: '_' })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/file-remote-delete.tsx: -------------------------------------------------------------------------------- 1 | import { msg, t } from '@lingui/core/macro'; 2 | import { FileRemoteAPI } from 'src/api'; 3 | import { DeleteRemoteModal } from 'src/components'; 4 | import { 5 | handleHttpError, 6 | parsePulpIDFromURL, 7 | taskAlert, 8 | waitForTaskUrl, 9 | } from 'src/utilities'; 10 | import { Action } from './action'; 11 | 12 | export const fileRemoteDeleteAction = Action({ 13 | title: msg`Delete`, 14 | modal: ({ addAlert, listQuery, setState, state }) => 15 | state.deleteModalOpen ? ( 16 | setState({ deleteModalOpen: null })} 18 | deleteAction={() => 19 | deleteRemote(state.deleteModalOpen, { addAlert, setState, listQuery }) 20 | } 21 | name={state.deleteModalOpen.name} 22 | /> 23 | ) : null, 24 | onClick: ( 25 | { name, id, pulp_href }: { name: string; id?: string; pulp_href?: string }, 26 | { setState }, 27 | ) => 28 | setState({ 29 | deleteModalOpen: { pulpId: id || parsePulpIDFromURL(pulp_href), name }, 30 | }), 31 | }); 32 | 33 | function deleteRemote({ name, pulpId }, { addAlert, setState, listQuery }) { 34 | return FileRemoteAPI.delete(pulpId) 35 | .then(({ data }) => { 36 | addAlert(taskAlert(data.task, t`Removal started for remote ${name}`)); 37 | setState({ deleteModalOpen: null }); 38 | return waitForTaskUrl(data.task); 39 | }) 40 | .then(() => listQuery()) 41 | .catch( 42 | handleHttpError(t`Failed to remove remote ${name}`, () => null, addAlert), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/actions/file-remote-edit.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const fileRemoteEditAction = Action({ 6 | title: msg`Edit`, 7 | onClick: ({ name }, { navigate }) => 8 | navigate(formatPath(Paths.file.remote.edit, { name })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/file-repository-create.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const fileRepositoryCreateAction = Action({ 6 | title: msg`Add repository`, 7 | onClick: (item, { navigate }) => 8 | navigate(formatPath(Paths.file.repository.edit, { name: '_' })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/file-repository-edit.tsx: -------------------------------------------------------------------------------- 1 | import { msg } from '@lingui/core/macro'; 2 | import { Paths, formatPath } from 'src/paths'; 3 | import { Action } from './action'; 4 | 5 | export const fileRepositoryEditAction = Action({ 6 | title: msg`Edit`, 7 | onClick: ({ name }, { navigate }) => 8 | navigate(formatPath(Paths.file.repository.edit, { name })), 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export { ActionType } from './action'; 2 | export { ansibleRemoteCreateAction } from './ansible-remote-create'; 3 | export { ansibleRemoteDeleteAction } from './ansible-remote-delete'; 4 | export { ansibleRemoteDownloadCAAction } from './ansible-remote-download-ca'; 5 | export { ansibleRemoteDownloadClientAction } from './ansible-remote-download-client'; 6 | export { ansibleRemoteDownloadRequirementsAction } from './ansible-remote-download-requirements'; 7 | export { ansibleRemoteEditAction } from './ansible-remote-edit'; 8 | export { ansibleRepositoryCollectionVersionAddAction } from './ansible-repository-collection-version-add'; 9 | export { ansibleRepositoryCollectionVersionRemoveAction } from './ansible-repository-collection-version-remove'; 10 | export { ansibleRepositoryCopyAction } from './ansible-repository-copy'; 11 | export { ansibleRepositoryCreateAction } from './ansible-repository-create'; 12 | export { ansibleRepositoryDeleteAction } from './ansible-repository-delete'; 13 | export { ansibleRepositoryEditAction } from './ansible-repository-edit'; 14 | export { ansibleRepositorySyncAction } from './ansible-repository-sync'; 15 | export { ansibleRepositoryVersionRevertAction } from './ansible-repository-version-revert'; 16 | export { fileRemoteCreateAction } from './file-remote-create'; 17 | export { fileRemoteDeleteAction } from './file-remote-delete'; 18 | export { fileRemoteEditAction } from './file-remote-edit'; 19 | export { fileRepositoryCreateAction } from './file-repository-create'; 20 | export { fileRepositoryDeleteAction } from './file-repository-delete'; 21 | export { fileRepositoryEditAction } from './file-repository-edit'; 22 | export { fileRepositorySyncAction } from './file-repository-sync'; 23 | -------------------------------------------------------------------------------- /src/api/activities.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | // FIXME HubAPI 6 | export const ActivitiesAPI = { 7 | listRepo: (id, params?) => 8 | base.list( 9 | `v3/plugin/execution-environments/repositories/${id}/_content/history/`, 10 | params, 11 | ), 12 | }; 13 | -------------------------------------------------------------------------------- /src/api/ansible-distribution.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const AnsibleDistributionAPI = { 6 | create: (data) => base.http.post(`distributions/ansible/ansible/`, data), 7 | 8 | delete: (id) => base.http.delete(`distributions/ansible/ansible/${id}/`), 9 | 10 | list: (params?) => base.list(`distributions/ansible/ansible/`, params), 11 | 12 | url: (distro_data) => distro_data.client_url, 13 | }; 14 | -------------------------------------------------------------------------------- /src/api/base.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ParamHelper } from 'src/utilities'; 3 | 4 | export class BaseAPI { 5 | http: { delete; get; interceptors; patch; post; put }; 6 | sortParam: string; // translate ?sort into sortParam in list() 7 | mapPageToOffset: boolean; 8 | 9 | constructor() { 10 | this.http = axios.create({ 11 | // adapter + withCredentials ensures no popup on http basic auth fail 12 | adapter: 'fetch', 13 | withCredentials: false, 14 | 15 | // baseURL gets set in PulpAPI 16 | paramsSerializer: { 17 | serialize: (params) => ParamHelper.getQueryString(params), 18 | }, 19 | }); 20 | } 21 | 22 | mapParams(params) { 23 | const newParams = { ...params }; 24 | 25 | if (this.mapPageToOffset) { 26 | // The api uses offset/limit OR page/page_size for pagination 27 | // the UI uses page/page size and maps to whatever the api expects 28 | 29 | const pageSize = parseInt(newParams['page_size'], 10) || 10; 30 | const page = parseInt(newParams['page'], 10) || 1; 31 | 32 | delete newParams['page']; 33 | delete newParams['page_size']; 34 | 35 | newParams['offset'] = page * pageSize - pageSize; 36 | newParams['limit'] = pageSize; 37 | } 38 | 39 | if (this.sortParam && newParams['sort'] && this.sortParam !== 'sort') { 40 | // The api uses sort/ordering/order_by for sort 41 | // the UI uses sort and maps to whatever the api expects 42 | 43 | newParams[this.sortParam] = newParams['sort']; 44 | delete newParams['sort']; 45 | } 46 | 47 | return { 48 | params: newParams, 49 | }; 50 | } 51 | 52 | list(url: string, params?) { 53 | return this.http.get(url, this.mapParams(params)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/api/certificate-upload.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | interface UploadProps { 4 | file: File; 5 | // Takes pulp_href for repository 6 | repository: string; 7 | // Takes pulp_href for collection 8 | signed_collection: string; 9 | } 10 | 11 | const base = new PulpAPI(); 12 | 13 | export const CertificateUploadAPI = { 14 | // Returns /pulp/api/v3/tasks/0be64cb4-3b7e-4a6b-b35d-c3b589923a90/ 15 | upload: (data: UploadProps): Promise<{ data: { task: string } }> => { 16 | const formData = new FormData(); 17 | formData.append('file', data.file); 18 | formData.append('repository', data.repository); 19 | formData.append('signed_collection', data.signed_collection); 20 | 21 | const config = { 22 | headers: { 23 | 'Content-Type': 'multipart/form-data', 24 | }, 25 | }; 26 | 27 | return base.http.post( 28 | `content/ansible/collection_signatures/`, 29 | formData, 30 | config, 31 | ); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/api/collection-version.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const CollectionVersionAPI = { 6 | copy: ( 7 | namespace: string, 8 | name: string, 9 | version: string, 10 | source_base_path: string, 11 | destination_base_path: string, 12 | ) => 13 | // FIXME HubAPI 14 | base.http.post( 15 | `v3/collections/${namespace}/${name}/versions/${version}/copy/${source_base_path}/${destination_base_path}/`, 16 | {}, 17 | ), 18 | 19 | get: (id: string) => 20 | base.http.get(`content/ansible/collection_versions/${id}/`), 21 | 22 | getUsedDependenciesByCollection: (namespace, collection, params = {}) => 23 | // FIXME HubAPI 24 | base.list( 25 | `v3/plugin/ansible/search/collection-versions/?dependency=${namespace}.${collection}`, 26 | params, 27 | ), 28 | 29 | list: (params?) => base.list(`content/ansible/collection_versions/`, params), 30 | 31 | move: ( 32 | namespace: string, 33 | name: string, 34 | version: string, 35 | source_base_path: string, 36 | destination_base_path: string, 37 | ) => 38 | // FIXME HubAPI 39 | base.http.post( 40 | `v3/collections/${namespace}/${name}/versions/${version}/move/${source_base_path}/${destination_base_path}/`, 41 | {}, 42 | ), 43 | }; 44 | -------------------------------------------------------------------------------- /src/api/container-distribution.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const ContainerDistributionAPI = { 6 | patch: (id, data) => 7 | base.http.patch(`distributions/container/container/${id}/`, data), 8 | 9 | list: (params?) => base.list(`distributions/container/container/`, params), 10 | 11 | url: (distro_data) => distro_data.registry_path, 12 | }; 13 | 14 | export const ContainerPullThroughDistributionAPI = { 15 | list: (params?) => base.list(`distributions/container/pull-through/`, params), 16 | 17 | // We should probably put this into a field on the serializer 18 | url: (distro_data) => `${window.location.host}/${distro_data.base_path}/`, 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/container-tag.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const ContainerTagAPI = { 6 | tag: (repositoryID: string, tag: string, digest: string) => 7 | base.http.post( 8 | `repositories/container/container-push/${repositoryID}/tag/`, 9 | { 10 | digest, 11 | tag, 12 | }, 13 | ), 14 | 15 | untag: (repositoryID: string, tag: string) => 16 | base.http.post( 17 | `repositories/container/container-push/${repositoryID}/untag/`, 18 | { 19 | tag, 20 | }, 21 | ), 22 | }; 23 | -------------------------------------------------------------------------------- /src/api/execution-environment-namespace.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | base.sortParam = 'sort'; // FIXME 5 | 6 | export const ExecutionEnvironmentNamespaceAPI = { 7 | addRole: (id, role) => 8 | base.http.post(`pulp_container/namespaces/${id}/add_role/`, role), 9 | 10 | listRoles: (id, params?) => 11 | base.list(`pulp_container/namespaces/${id}/list_roles/`, params), 12 | 13 | myPermissions: (id, params?) => 14 | base.list(`pulp_container/namespaces/${id}/my_permissions/`, params), 15 | 16 | removeRole: (id, role) => 17 | base.http.post(`pulp_container/namespaces/${id}/remove_role/`, role), 18 | }; 19 | -------------------------------------------------------------------------------- /src/api/execution-environment-remote.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | // FIXME HubAPI 6 | export const ExecutionEnvironmentRemoteAPI = { 7 | create: (data) => 8 | base.http.post(`_ui/v1/execution-environments/remotes/`, data), 9 | 10 | get: (id) => base.http.get(`_ui/v1/execution-environments/remotes/${id}/`), 11 | 12 | list: (params?) => 13 | base.list(`_ui/v1/execution-environments/remotes/`, params), 14 | 15 | sync: (name) => 16 | base.http.post( 17 | `v3/plugin/execution-environments/repositories/${name}/_content/sync/`, 18 | {}, 19 | ), 20 | 21 | update: (id, data) => 22 | base.http.put(`_ui/v1/execution-environments/remotes/${id}/`, data), 23 | }; 24 | -------------------------------------------------------------------------------- /src/api/execution-environment.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | // FIXME HubAPI 6 | export const ExecutionEnvironmentAPI = { 7 | deleteExecutionEnvironment: (name) => 8 | base.http.delete(`v3/plugin/execution-environments/repositories/${name}/`), 9 | 10 | deleteImage: (name, manifest) => 11 | base.http.delete( 12 | `v3/plugin/execution-environments/repositories/${name}/_content/images/${manifest}/`, 13 | ), 14 | 15 | get: (id) => 16 | base.http.get(`v3/plugin/execution-environments/repositories/${id}/`), 17 | 18 | image: (name, digest) => 19 | base.http.get( 20 | `v3/plugin/execution-environments/repositories/${name}/_content/images/${digest}/`, 21 | ), 22 | 23 | images: (name, params) => 24 | base.list( 25 | `v3/plugin/execution-environments/repositories/${name}/_content/images/`, 26 | params, 27 | ), 28 | 29 | list: (params?) => 30 | base.list(`v3/plugin/execution-environments/repositories/`, params), 31 | 32 | readme: (name) => 33 | base.http.get( 34 | `v3/plugin/execution-environments/repositories/${name}/_content/readme/`, 35 | ), 36 | 37 | saveReadme: (name, readme) => 38 | base.http.put( 39 | `v3/plugin/execution-environments/repositories/${name}/_content/readme/`, 40 | readme, 41 | ), 42 | 43 | tags: (name, params) => 44 | base.list( 45 | `v3/plugin/execution-environments/repositories/${name}/_content/tags/`, 46 | params, 47 | ), 48 | }; 49 | -------------------------------------------------------------------------------- /src/api/file-content.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const FileContentAPI = { 6 | create: (data) => base.http.post(`content/file/files/`, data), 7 | 8 | get: (href) => base.http.get(href), 9 | 10 | list: (params?) => base.list(`content/file/files/`, params), 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/file-distribution.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const FileDistributionAPI = { 6 | create: (data) => base.http.post(`distributions/file/file/`, data), 7 | 8 | delete: (id) => base.http.delete(`distributions/file/file/${id}/`), 9 | 10 | list: (params?) => base.list(`distributions/file/file/`, params), 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/file-publication.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const FilePublicationAPI = { 6 | create: (data) => base.http.post(`publications/file/file/`, data), 7 | 8 | delete: (id) => base.http.delete(`publications/file/file/${id}/`), 9 | 10 | list: (params?) => base.list(`publications/file/file/`, params), 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/file-remote.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | export class FileRemoteType { 4 | ca_cert: string; 5 | client_cert: string; 6 | download_concurrency: number; 7 | name: string; 8 | proxy_url: string; 9 | pulp_href?: string; 10 | rate_limit: number; 11 | tls_validation: boolean; 12 | url: string; 13 | sync_dependencies?: boolean; 14 | 15 | // connect_timeout 16 | // headers 17 | // max_retries 18 | // policy 19 | // prn 20 | // pulp_created 21 | // pulp_labels 22 | // pulp_last_updated 23 | // sock_connect_timeout 24 | // sock_read_timeout 25 | // total_timeout 26 | 27 | hidden_fields: { 28 | is_set: boolean; 29 | name: string; 30 | }[]; 31 | 32 | my_permissions?: string[]; 33 | } 34 | 35 | // simplified version of smartUpdate from execution-environment-registry 36 | function smartUpdate(remote: FileRemoteType, unmodifiedRemote: FileRemoteType) { 37 | for (const field of Object.keys(remote)) { 38 | if (remote[field] === '') { 39 | remote[field] = null; 40 | } 41 | 42 | // API returns headers:null bull doesn't accept it .. and we don't edit headers 43 | if (remote[field] === null && unmodifiedRemote[field] === null) { 44 | delete remote[field]; 45 | } 46 | } 47 | 48 | return remote; 49 | } 50 | 51 | const base = new PulpAPI(); 52 | 53 | export const FileRemoteAPI = { 54 | create: (data) => base.http.post(`remotes/file/file/`, data), 55 | 56 | delete: (id) => base.http.delete(`remotes/file/file/${id}/`), 57 | 58 | get: (id) => base.http.get(`remotes/file/file/${id}/`), 59 | 60 | list: (params?) => base.list(`remotes/file/file/`, params), 61 | 62 | smartUpdate: (id, newValue: FileRemoteType, oldValue: FileRemoteType) => 63 | base.http.put(`remotes/file/file/${id}/`, smartUpdate(newValue, oldValue)), 64 | }; 65 | -------------------------------------------------------------------------------- /src/api/file-repository.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | export class FileRepositoryType { 4 | autopublish?: boolean; 5 | description: string | null; 6 | latest_version_href?: string; 7 | manifest?: string; 8 | name: string; 9 | prn?: string; 10 | pulp_created?: string; 11 | pulp_href?: string; 12 | pulp_labels: Record; 13 | pulp_last_updated?: string; 14 | remote: string | null; 15 | retain_repo_versions: number; 16 | versions_href?: string; 17 | } 18 | 19 | const base = new PulpAPI(); 20 | 21 | export const FileRepositoryAPI = { 22 | create: (data) => base.http.post(`repositories/file/file/`, data), 23 | 24 | delete: (id) => base.http.delete(`repositories/file/file/${id}/`), 25 | 26 | list: (params?) => base.list(`repositories/file/file/`, params), 27 | 28 | listVersions: (id: string, params?) => 29 | base.list(`repositories/file/file/${id}/versions/`, params), 30 | 31 | revert: (id: string, version_href) => 32 | base.http.post(`repositories/file/file/${id}/modify/`, { 33 | base_version: version_href, 34 | }), 35 | 36 | sync: (id: string, body = {}) => 37 | base.http.post(`repositories/file/file/${id}/sync/`, body), 38 | 39 | update: (id: string, data) => 40 | base.http.put(`repositories/file/file/${id}/`, data), 41 | }; 42 | -------------------------------------------------------------------------------- /src/api/generic-pulp.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const GenericPulpAPI = { 6 | get: (url: string) => base.http.get(url), 7 | 8 | list: (url: string, params = {}) => base.list(url, params), 9 | }; 10 | 11 | export const GenericContentAPI = { 12 | list: (params?) => base.list(`content/`, params), 13 | }; 14 | 15 | export const GenericDistributionAPI = { 16 | list: (params?) => base.list(`distributions/`, params), 17 | }; 18 | 19 | export const GenericPublicationAPI = { 20 | list: (params?) => base.list(`publications/`, params), 21 | }; 22 | 23 | export const GenericRemoteAPI = { 24 | list: (params?) => base.list(`remotes/`, params), 25 | }; 26 | 27 | export const GenericRepositoryAPI = { 28 | list: (params?) => base.list(`repositories/`, params), 29 | }; 30 | 31 | export const GenericRepositoryVersionAPI = { 32 | list: (params?) => base.list(`repository_versions/`, params), 33 | }; 34 | -------------------------------------------------------------------------------- /src/api/get-cancel-token.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const getCancelToken = () => axios.CancelToken.source(); 4 | -------------------------------------------------------------------------------- /src/api/group-role.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const GroupRoleAPI = { 6 | addRoleToGroup: (groupId, role) => 7 | base.http.post(`groups/${groupId}/roles/`, { 8 | role: role.name, 9 | // required field, can be empty 10 | content_object: null, 11 | }), 12 | 13 | listRoles: (groupId, params?) => 14 | base.list(`groups/${groupId}/roles/`, params), 15 | 16 | removeRole: (groupId, roleId) => 17 | base.http.delete(`groups/${groupId}/roles/${roleId}/`), 18 | }; 19 | -------------------------------------------------------------------------------- /src/api/group.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const GroupAPI = { 6 | addUserToGroup: (id, username) => 7 | base.http.post(`groups/${id}/users/`, { 8 | username: username, 9 | }), 10 | 11 | create: (data) => base.http.post(`groups/`, data), 12 | 13 | delete: (id) => base.http.delete(`groups/${id}/`), 14 | 15 | get: (id) => base.http.get(`groups/${id}/`), 16 | 17 | getUsers: (id) => base.http.get(`groups/${id}/users/`), 18 | 19 | list: (params?) => base.list(`groups/`, params), 20 | 21 | update: (id, data) => base.http.put(`groups/${id}/`, data), 22 | }; 23 | -------------------------------------------------------------------------------- /src/api/import.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | // FIXME HubAPI 6 | export const ImportAPI = { 7 | get: (id) => base.http.get(`_ui/v1/imports/collections/${id}/`), 8 | 9 | list: (params?) => base.list(`_ui/v1/imports/collections/`, params), 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/namespace.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | // FIXME HubAPI 6 | export const NamespaceAPI = { 7 | create: (data) => base.http.post(`_ui/v1/namespaces/`, data), 8 | 9 | delete: (id) => base.http.delete(`_ui/v1/namespaces/${id}/`), 10 | 11 | get: (id: string, params = {}) => 12 | base.http.get(`_ui/v1/namespaces/${id}/`, { params }), 13 | 14 | list: (params?) => base.list(`_ui/v1/namespaces/`, params), 15 | 16 | update: (id, data) => base.http.put(`_ui/v1/namespaces/${id}/`, data), 17 | }; 18 | -------------------------------------------------------------------------------- /src/api/orphan-cleanup.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const OrphanCleanupAPI = { 6 | create: (data) => base.http.post(`orphans/cleanup/`, data), 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/pulp-login.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const PulpLoginAPI = { 6 | try: (username, password) => 7 | // roles = any api that will always be there and requires auth 8 | base.http.get(`roles/`, { auth: { username, password } }), 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/pulp-status.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const PulpStatusAPI = { 6 | get: () => base.http.get(`status/`), 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/pulp.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | import { config } from 'src/ui-config'; 3 | import { BaseAPI } from './base'; 4 | 5 | export class PulpAPI extends BaseAPI { 6 | mapPageToOffset = true; // offset & limit 7 | sortParam = 'ordering'; 8 | 9 | constructor() { 10 | super(); 11 | 12 | this.http.interceptors.request.use((request) => { 13 | if (!request.auth) { 14 | request.auth = JSON.parse( 15 | window.sessionStorage.credentials || 16 | window.localStorage.credentials || 17 | '{}', 18 | ); 19 | } 20 | 21 | request.baseURL = config.API_BASE_PATH; 22 | request.headers['X-CSRFToken'] = Cookies.get('csrftoken'); 23 | 24 | return request; 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/api/repair.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const RepairAPI = { 6 | create: (data) => base.http.post(`repair/`, data), 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/response-types/execution-environment.ts: -------------------------------------------------------------------------------- 1 | import { type LastSyncType } from './remote'; 2 | 3 | export class ContainerManifestType { 4 | pulp_id: string; 5 | created_at: string; 6 | digest: string; 7 | schema_version: number; 8 | config_blob: { 9 | digest: string; 10 | media_type: string; 11 | data?: unknown; 12 | }; 13 | tags: string[]; 14 | layers: { digest: string; size: number }[]; 15 | } 16 | 17 | export class ContainerRepositoryType { 18 | id: string; 19 | name: string; 20 | pulp: { 21 | repository: { 22 | id: string; 23 | pulp_type: string; 24 | version: string; 25 | name: string; 26 | description: string; 27 | created_at: string; 28 | updated_at: string; 29 | last_sync_task: string; 30 | pulp_labels: object; 31 | remote?: { 32 | id: string; 33 | pulp_href: string; 34 | registry: string; 35 | upstream_name: string; 36 | include_tags: string[]; 37 | exclude_tags: string[]; 38 | last_sync_task: LastSyncType; 39 | created_at: string; 40 | updated_at: string; 41 | }; 42 | sign_state: string; 43 | }; 44 | distribution: { 45 | id: string; 46 | base_path: string; 47 | name: string; 48 | created_at: string; 49 | updated_at: string; 50 | pulp_labels: object; 51 | }; 52 | }; 53 | namespace: { 54 | id: string; 55 | pulp_href: string; 56 | name: string; 57 | my_permissions: string[]; 58 | owners: string[]; 59 | created_at: string; 60 | updated_at: string; 61 | }; 62 | description: string; 63 | created_at: string; 64 | updated_at: string; 65 | } 66 | -------------------------------------------------------------------------------- /src/api/response-types/import.ts: -------------------------------------------------------------------------------- 1 | import { type PulpStatus } from './pulp'; 2 | 3 | export class ImportListType { 4 | id: number; 5 | state: PulpStatus; 6 | started_at: string; 7 | finished_at: string; 8 | namespace: string; 9 | // Collection name 10 | name: string; 11 | version: string; 12 | collectionRepo: string; 13 | } 14 | 15 | export class ImportDetailType extends ImportListType { 16 | error?: { 17 | code: string; 18 | description: string; 19 | traceback: string; 20 | }; 21 | 22 | job_id: string; 23 | imported_version: string; 24 | messages: { 25 | level: string; 26 | message: string; 27 | time: string; 28 | }[]; 29 | } 30 | -------------------------------------------------------------------------------- /src/api/response-types/namespace.ts: -------------------------------------------------------------------------------- 1 | import { type GroupObjectPermissionType } from './permissions'; 2 | 3 | export class NamespaceLinkType { 4 | name: string; 5 | url: string; 6 | } 7 | 8 | export class NamespaceListType { 9 | id: number; 10 | name: string; 11 | company: string; 12 | email: string; 13 | avatar_url: string; 14 | description: string; 15 | num_collections: number; 16 | } 17 | 18 | export class NamespaceType extends NamespaceListType { 19 | groups: GroupObjectPermissionType[]; 20 | users: { username: string; object_roles: string[] }[]; 21 | resources: string; 22 | owners: string[]; 23 | links: NamespaceLinkType[]; 24 | related_fields: { my_permissions?: string[] }; 25 | } 26 | -------------------------------------------------------------------------------- /src/api/response-types/permissions.ts: -------------------------------------------------------------------------------- 1 | export class GroupObjectPermissionType { 2 | id: number; 3 | name: string; 4 | object_permissions: string[]; 5 | pulp_href?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/api/response-types/pulp.ts: -------------------------------------------------------------------------------- 1 | export enum PulpStatus { 2 | waiting = 'waiting', 3 | skipped = 'skipped', 4 | running = 'running', 5 | completed = 'completed', 6 | failed = 'failed', 7 | canceled = 'canceled', 8 | } 9 | -------------------------------------------------------------------------------- /src/api/response-types/remote.ts: -------------------------------------------------------------------------------- 1 | import { type PulpStatus } from './pulp'; 2 | 3 | export class LastSyncType { 4 | state: PulpStatus; 5 | started_at: string; 6 | finished_at: string; 7 | error: { 8 | traceback: string; 9 | description: string; 10 | }; 11 | } 12 | 13 | export class RemoteType { 14 | id: string; 15 | pulp_href: string; 16 | name: string; 17 | url: string; 18 | signed_only: boolean; 19 | auth_url: string; 20 | rate_limit: number; 21 | token?: string; 22 | policy: string; 23 | requirements_file: string; 24 | updated_at: string; 25 | created_at: string; 26 | username: string; 27 | password?: string; 28 | proxy_url?: string; 29 | proxy_password?: string; 30 | proxy_username?: string; 31 | tls_validation?: boolean; 32 | client_key?: string; 33 | client_cert?: string; 34 | ca_cert?: string; 35 | sync_dependencies?: boolean; 36 | 37 | hidden_fields: { name: string; is_set: boolean }[]; 38 | 39 | repositories: { 40 | name: string; 41 | description: string; 42 | last_sync_task: LastSyncType; 43 | distributions: { 44 | name: string; 45 | base_path: string; 46 | }[]; 47 | }[]; 48 | last_sync_task: LastSyncType; 49 | download_concurrency: number; 50 | } 51 | -------------------------------------------------------------------------------- /src/api/response-types/role.ts: -------------------------------------------------------------------------------- 1 | export class RoleType { 2 | pulp_href: string; 3 | pulp_created: string; 4 | name: string; 5 | description: string; 6 | permissions?: string[]; 7 | locked: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/response-types/task.ts: -------------------------------------------------------------------------------- 1 | import { type PulpStatus } from './pulp'; 2 | 3 | export class TaskType { 4 | pulp_created: string; 5 | state: PulpStatus; 6 | name: string; 7 | started_at: string; 8 | finished_at: string; 9 | error: { traceback: string; description: string }; 10 | pulp_href: string; 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | progress_reports: any[]; 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | task_group: any; 15 | parent_task: string; 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | child_tasks: any[]; 18 | reserved_resources_record: string[]; 19 | } 20 | -------------------------------------------------------------------------------- /src/api/response-types/user.ts: -------------------------------------------------------------------------------- 1 | export class ModelPermissionsType { 2 | [key: string]: { 3 | global_description: string; 4 | has_model_permission: boolean; 5 | name: string; 6 | object_description: string; 7 | ui_category: string; 8 | }; 9 | } 10 | 11 | export class GroupType { 12 | id: number; 13 | name: string; 14 | object_roles?: string[]; 15 | } 16 | 17 | export class UserType { 18 | auth_provider?: string[]; 19 | date_joined?: string; 20 | email?: string; 21 | first_name?: string; 22 | groups: GroupType[]; 23 | hidden_fields?; 24 | id?: number; 25 | is_active?: boolean; 26 | is_staff?: boolean; 27 | last_name?: string; 28 | model_permissions?: ModelPermissionsType; 29 | password?: string; 30 | prn?: string; 31 | pulp_href?: string; 32 | username: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/api/role.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'src/ui-config'; 2 | import { PulpAPI } from './pulp'; 3 | 4 | const base = new PulpAPI(); 5 | 6 | export const RoleAPI = { 7 | addPermission: (id, data) => 8 | base.http.post(`roles/${id}/model-permissions/`, data), 9 | 10 | create: (data) => base.http.post(`roles/`, data), 11 | 12 | delete: (id) => base.http.delete(`roles/${id}/`), 13 | 14 | get: (id) => base.http.get(`roles/${id}/`), 15 | 16 | getPermissions: (id) => 17 | base.http.get(`roles/${id}/model-permissions/?limit=100000&offset=0`), 18 | 19 | list: (params?, for_object_type?) => { 20 | const newParams = { ...params }; 21 | if (for_object_type) { 22 | // ?for_object_type=/pulp/api/v3/.../ 23 | // list visible in http://localhost:8002/pulp/api/v3/ 24 | newParams.for_object_type = `${config.API_BASE_PATH}${for_object_type}/`; 25 | } 26 | return base.list(`roles/`, newParams); 27 | }, 28 | 29 | removePermission: (id, permissionId) => 30 | base.http.delete(`roles/${id}/model-permissions/${permissionId}/`), 31 | 32 | updatePermissions: (id, data) => base.http.patch(`roles/${id}/`, data), 33 | }; 34 | -------------------------------------------------------------------------------- /src/api/rpm-package.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const RPMPackageAPI = { 6 | list: (params?) => base.list(`content/rpm/packages/`, params), 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/rpm-repository.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const RPMRepositoryAPI = { 6 | list: (params?) => base.list(`repositories/rpm/rpm/`, params), 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/sign-collections.ts: -------------------------------------------------------------------------------- 1 | import { repositoryBasePath } from 'src/utilities'; 2 | import { PulpAPI } from './pulp'; 3 | import { type CollectionVersionSearch } from './response-types/collection'; 4 | 5 | interface SignNamespace { 6 | signing_service?: string; 7 | repository?: CollectionVersionSearch['repository']; 8 | repository_name?: string; 9 | namespace: string; 10 | } 11 | 12 | interface SignCollection extends SignNamespace { 13 | collection?: string; 14 | } 15 | 16 | interface SignCollectionVersion extends SignCollection { 17 | version?: string; 18 | } 19 | 20 | type SignProps = SignNamespace | SignCollection | SignCollectionVersion; 21 | 22 | const base = new PulpAPI(); 23 | 24 | // FIXME HubAPI 25 | export const SignCollectionAPI = { 26 | sign: ({ repository, repository_name: name, ...args }: SignProps) => 27 | repositoryBasePath(name, repository?.pulp_href) 28 | .catch((status) => 29 | Promise.reject({ 30 | response: { status }, 31 | }), 32 | ) 33 | .then((distro_base_path) => 34 | base.http.post(`_ui/v1/collection_signing/`, { 35 | distro_base_path, 36 | ...args, 37 | }), 38 | ), 39 | }; 40 | -------------------------------------------------------------------------------- /src/api/sign-containers.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const SignContainersAPI = { 6 | getSigningService: (serviceName: string) => 7 | base.http.get(`signing-services/?name=${serviceName}`), 8 | 9 | sign: ( 10 | containerId: string, 11 | pulp_type: string, 12 | signServicePath: string, 13 | base_path: string, 14 | ) => { 15 | const postObj = { manifest_signing_service: signServicePath }; 16 | if (pulp_type === 'container') { 17 | postObj['future_base_path'] = base_path; 18 | } 19 | 20 | return base.http.post( 21 | `repositories/container/${pulp_type}/${containerId}/sign/`, 22 | postObj, 23 | ); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/api/signing-service.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | export class SigningServiceType { 4 | name: string; 5 | pubkey_fingerprint: string; 6 | public_key: string; 7 | pulp_created: string; 8 | pulp_href: string; 9 | script: string; 10 | } 11 | 12 | const base = new PulpAPI(); 13 | 14 | export const SigningServiceAPI = { 15 | list: (params?) => base.list(`signing-services/`, params), 16 | }; 17 | -------------------------------------------------------------------------------- /src/api/tag.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | // FIXME HubAPI 6 | export const TagAPI = { 7 | listCollections: (params) => base.list(`_ui/v1/tags/collections/`, params), 8 | 9 | listRoles: (params) => base.list(`_ui/v1/tags/roles/`, params), 10 | }; 11 | -------------------------------------------------------------------------------- /src/api/task-purge.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const TaskPurgeAPI = { 6 | create: (data) => base.http.post(`tasks/purge/`, data), 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/task.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const TaskAPI = { 6 | get: (id) => base.http.get(`tasks/${id}/`), 7 | 8 | list: (params?) => base.list(`tasks/`, params), 9 | 10 | patch: (id, data) => base.http.patch(`tasks/${id}/`, data), 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { PulpAPI } from './pulp'; 2 | 3 | const base = new PulpAPI(); 4 | 5 | export const UserAPI = { 6 | create: (user) => base.http.post(`users/`, user), 7 | 8 | delete: (id) => base.http.delete(`users/${id}/`), 9 | 10 | get: (id) => base.http.get(`users/${id}/`), 11 | 12 | list: (params?) => base.list(`users/`, params), 13 | 14 | patch: (id, user) => base.http.patch(`users/${id}/`, user), 15 | 16 | saveUser: (user) => { 17 | const newUser = { ...user }; 18 | 19 | // do not reset password when not trying to change it 20 | if (!newUser.password) { 21 | delete newUser.password; 22 | } 23 | 24 | // delete non-editable fields 25 | delete newUser.date_joined; 26 | delete newUser.hidden_fields; 27 | delete newUser.id; 28 | delete newUser.is_active; 29 | delete newUser.is_staff; 30 | delete newUser.prn; 31 | delete newUser.pulp_href; 32 | 33 | return base.http.patch(`users/${user.id}/`, newUser); 34 | }, 35 | 36 | update: (id, user) => base.http.put(`users/${id}/`, user), 37 | }; 38 | -------------------------------------------------------------------------------- /src/app-context.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode, createContext, useContext, useState } from 'react'; 2 | import { type AlertType } from 'src/components'; 3 | import { useUserContext } from './user-context'; 4 | 5 | export interface IAppContextType { 6 | alerts: AlertType[]; 7 | featureFlags; // deprecated 8 | hasPermission: (name: string) => boolean; 9 | queueAlert: (alert: AlertType) => void; 10 | setAlerts: (alerts: AlertType[]) => void; 11 | settings; // deprecated 12 | user; // deprecated 13 | } 14 | 15 | export const AppContext = createContext(undefined); 16 | export const useAppContext = () => useContext(AppContext); 17 | 18 | export const AppContextProvider = ({ children }: { children: ReactNode }) => { 19 | const [alerts, setAlerts] = useState([]); 20 | const { credentials } = useUserContext(); 21 | 22 | // hub compat for now 23 | const featureFlags = { 24 | container_signing: true, 25 | execution_environments: true, 26 | external_authentication: false, 27 | display_repositories: true, 28 | can_create_signatures: true, 29 | can_upload_signatures: true, 30 | collection_auto_sign: true, 31 | collection_signing: true, 32 | display_signatures: true, 33 | require_upload_signatures: false, 34 | }; 35 | const settings = { 36 | GALAXY_COLLECTION_SIGNING_SERVICE: 'mock_signing', 37 | GALAXY_CONTAINER_SIGNING_SERVICE: 'mock_signing', 38 | }; 39 | 40 | const queueAlert = (alert) => setAlerts((alerts) => [...alerts, alert]); 41 | const hasPermission = (_name) => true; // FIXME: permission handling 42 | 43 | return ( 44 | 62 | {children} 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | // Patternfly expects the root Page component to be the height of the browser window 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | 7 | .pulp-section { 8 | background-color: var(--pf-v5-global--BackgroundColor--100); 9 | padding: 16px; 10 | } 11 | 12 | .pulp-clickable { 13 | cursor: pointer; 14 | } 15 | 16 | // remove padding between menu and top navbar 17 | #page-sidebar .pf-v5-c-nav { 18 | --pf-v5-c-nav__list--PaddingTop: 0; 19 | } 20 | 21 | // this was getting applied globally when it was in the collection-info.scss file 22 | // which means other components now rely on it, so I'm just going move it here 23 | // so that it's explicitly setting these styles globally. 24 | pre { 25 | background-color: var(--pf-v5-global--BackgroundColor--150); 26 | border: 1px solid var(--pf-v5-global--BackgroundColor--200); 27 | font-family: var(--pf-v5-global--FontFamily--monospace); 28 | display: block; 29 | padding: 10px; 30 | 31 | // Patternfly does something that makes it so that fonts don't get inherited 32 | // from the parent so it has to be overridden at every level. 33 | code { 34 | font-family: var(--pf-v5-global--FontFamily--monospace); 35 | } 36 | } 37 | 38 | // list screen toolbars 39 | .pulp-toolbar { 40 | display: flex; 41 | justify-content: space-between; 42 | align-items: center; 43 | } 44 | 45 | // inline alert inside pf-v5-c-content has big title and extra margin otherwise 46 | .pf-v5-c-content .pf-v5-c-alert__title { 47 | margin: 0; 48 | font-size: var(--pf-v5-c-alert__title--FontSize); 49 | font-weight: var(--pf-v5-c-alert__title--FontWeight); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/alert-list.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertActionCloseButton, 3 | type AlertProps, 4 | } from '@patternfly/react-core'; 5 | import { type ReactNode } from 'react'; 6 | import { Alert } from 'src/components'; 7 | 8 | interface IProps { 9 | /** List of alerts to display */ 10 | alerts: AlertType[]; 11 | 12 | /** Callback to close the alert at the given index */ 13 | closeAlert: (alertIndex) => void; 14 | } 15 | 16 | export interface AlertType { 17 | id?: string; 18 | variant: AlertProps['variant']; 19 | title: string | ReactNode; 20 | description?: string | ReactNode; 21 | } 22 | 23 | export const AlertList = ({ alerts, closeAlert }: IProps) => ( 24 |
35 | {alerts.map((alert, i) => ( 36 | closeAlert(i)} />} 42 | > 43 | {alert.description} 44 | 45 | ))} 46 |
47 | ); 48 | 49 | export function closeAlert(alertIndex, { alerts, setAlerts }) { 50 | const newList = [...alerts]; 51 | newList.splice(alertIndex, 1); 52 | setAlerts(newList); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/applied-filters.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Button } from '@patternfly/react-core'; 3 | import { type CSSProperties } from 'react'; 4 | import { Chip, ChipGroup } from 'src/components'; 5 | import { ParamHelper, type ParamType } from 'src/utilities'; 6 | 7 | interface IProps { 8 | /** Sets the current page params to p */ 9 | updateParams: (p) => void; 10 | 11 | /** Current page params */ 12 | params: ParamType; 13 | 14 | /** A list of params that shouldn't get displayed */ 15 | ignoredParams?: string[]; 16 | 17 | /** 18 | * If k from param[k] is in nice names, use niceNames[k] instead of k 19 | * when displaying the param field name 20 | */ 21 | niceNames?: object; 22 | niceValues?: object; 23 | style?: CSSProperties; 24 | className?: string; 25 | } 26 | 27 | export const AppliedFilters = ({ 28 | className, 29 | ignoredParams = [], 30 | niceNames = {}, 31 | niceValues, 32 | params, 33 | style, 34 | updateParams, 35 | }: IProps) => { 36 | const filters = Object.keys(ParamHelper.getReduced(params, ignoredParams)); 37 | if (!filters.length) { 38 | return null; 39 | } 40 | 41 | const renderGroup = (key) => { 42 | const chips = Array.isArray(params[key]) 43 | ? (params[key] as string[] | number[]) 44 | : [params[key]]; 45 | 46 | const unsetFilter = (v) => 47 | updateParams({ 48 | ...ParamHelper.deleteParam(params, key, v), 49 | page: 1, 50 | }); 51 | 52 | return ( 53 |
54 | 55 | {chips.map((v, i) => ( 56 | unsetFilter(v)}> 57 | {niceValues?.[key]?.[v] || v} 58 | 59 | ))} 60 | 61 |
62 | ); 63 | }; 64 | 65 | return ( 66 |
67 | {filters.map((key) => renderGroup(key))} 68 | 76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/base-header.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from '@patternfly/react-core'; 2 | import { type ReactNode, useEffect } from 'react'; 3 | import './header.scss'; 4 | 5 | interface IProps { 6 | breadcrumbs?: ReactNode; 7 | children?: ReactNode; 8 | className?: string; 9 | contextSelector?: ReactNode; 10 | logo?: ReactNode; 11 | pageControls?: ReactNode; 12 | status?: ReactNode; 13 | subTitle?: ReactNode; 14 | title: string; 15 | versionControl?: ReactNode; 16 | } 17 | 18 | export const BaseHeader = ({ 19 | title, 20 | logo, 21 | pageControls, 22 | children, 23 | breadcrumbs, 24 | className, 25 | contextSelector, 26 | versionControl, 27 | status, 28 | subTitle, 29 | }: IProps) => { 30 | useEffect(() => { 31 | document.title = title ? `Pulp UI - ${title}` : 'Pulp UI'; 32 | }, [title]); 33 | 34 | return ( 35 |
42 | {contextSelector || null} 43 | {breadcrumbs && ( 44 |
45 | {breadcrumbs} 46 |
47 | )} 48 | {!breadcrumbs && !contextSelector &&
} 49 | 50 |
54 |
58 | {logo} 59 |
60 | 61 | {title} 62 | {status} 63 | 64 | {subTitle} 65 |
66 |
67 | {pageControls || null} 68 |
69 | {versionControl || null} 70 | 71 | {children ? ( 72 |
{children}
73 | ) : ( 74 |
75 | )} 76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { BreadcrumbItem } from '@patternfly/react-core'; 2 | import { Link } from 'react-router'; 3 | import { Breadcrumb } from 'src/components'; 4 | 5 | export interface BreadcrumbType { 6 | name: string; 7 | url?: string; 8 | } 9 | 10 | interface IProps { 11 | /** List of links to display in the breadcrumb */ 12 | links: BreadcrumbType[]; 13 | } 14 | 15 | export const Breadcrumbs = ({ links }: IProps) => ( 16 | 17 | {links.map((link, index) => ( 18 | 19 | {link.url ? {link.name} : link.name} 20 | 21 | ))} 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /src/components/card-list-switcher.tsx: -------------------------------------------------------------------------------- 1 | import ListIcon from '@patternfly/react-icons/dist/esm/icons/list-icon'; 2 | import ThLargeIcon from '@patternfly/react-icons/dist/esm/icons/th-large-icon'; 3 | import cx from 'classnames'; 4 | import { type FunctionComponent } from 'react'; 5 | import { Icon } from 'src/components'; 6 | import { ParamHelper } from 'src/utilities'; 7 | import './switcher.scss'; 8 | 9 | interface IProps { 10 | params: { 11 | view_type?: string; 12 | }; 13 | updateParams: (params) => void; 14 | } 15 | 16 | export const CardListSwitcher: FunctionComponent = ({ 17 | params, 18 | updateParams, 19 | }) => { 20 | const disp = params.view_type || 'card'; 21 | 22 | return ( 23 |
24 | 25 | 28 | updateParams(ParamHelper.setParam(params, 'view_type', 'card')) 29 | } 30 | > 31 | 32 | 33 | 34 | 35 | 38 | updateParams(ParamHelper.setParam(params, 'view_type', 'list')) 39 | } 40 | > 41 | 42 | 43 | 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/cards.scss: -------------------------------------------------------------------------------- 1 | .pulp-c-card-ns-container { 2 | width: 300px; 3 | 4 | .title { 5 | font-weight: bold; 6 | } 7 | 8 | .ns-name { 9 | text-overflow: ellipsis; 10 | overflow: hidden; 11 | white-space: nowrap; 12 | width: 180px; 13 | } 14 | } 15 | 16 | .pulp-c-card-collection-container { 17 | width: 280px; 18 | 19 | .card-badge-area { 20 | display: flex; 21 | gap: 5px; 22 | flex-wrap: wrap; 23 | place-content: stretch end; 24 | align-items: center; 25 | } 26 | 27 | .name { 28 | font-weight: bold; 29 | text-overflow: ellipsis; 30 | overflow: hidden; 31 | white-space: nowrap; 32 | width: 100%; 33 | } 34 | 35 | .author .pf-v5-c-content small { 36 | text-overflow: ellipsis; 37 | overflow: hidden; 38 | } 39 | 40 | .type-container { 41 | display: flex; 42 | justify-content: space-between; 43 | } 44 | 45 | .logo-row { 46 | padding-right: 8px; 47 | 48 | .pf-v5-c-card__header-main { 49 | display: flex; 50 | justify-content: space-between; 51 | } 52 | 53 | .icon { 54 | color: var(--pf-v5-global--info-color--100); 55 | margin-right: 3px; 56 | } 57 | } 58 | 59 | .description { 60 | height: 48px; 61 | overflow: hidden; 62 | } 63 | } 64 | 65 | .hidden-menu-space { 66 | margin-right: 8px; 67 | } 68 | 69 | .pulp-card-layout { 70 | display: flex; 71 | flex-wrap: wrap; 72 | 73 | .card-wrapper { 74 | margin-right: 24px; 75 | margin-bottom: 24px; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/collection-content-list.scss: -------------------------------------------------------------------------------- 1 | .pulp-c-toolbar__item-type-selector { 2 | margin-left: 16px; 3 | text-transform: capitalize; 4 | } 5 | 6 | .pulp-c-toolbar__item-selected-item, 7 | .pulp-c-toolbar__item-type-selector:hover { 8 | border-bottom: 2px solid var(--pf-v5-global--link--Color); 9 | padding-bottom: 2px; 10 | margin-bottom: -4px; 11 | background-color: white; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/collection-dependencies-list.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { List, ListItem } from '@patternfly/react-core'; 3 | import { Link } from 'react-router'; 4 | import { type CollectionVersion, type CollectionVersionSearch } from 'src/api'; 5 | import { EmptyStateNoData, HelpButton } from 'src/components'; 6 | 7 | interface IProps { 8 | collection: CollectionVersionSearch; 9 | dependencies_repos?: (CollectionVersion & { 10 | path?: string; 11 | })[]; 12 | } 13 | 14 | export const CollectionDependenciesList = ({ 15 | collection, 16 | dependencies_repos, 17 | }: IProps) => { 18 | const { dependencies } = collection.collection_version; 19 | 20 | if (!Object.keys(dependencies).length) { 21 | return ( 22 | 26 | ); 27 | } 28 | 29 | return ( 30 | 31 | {dependencies_repos.map((dependency, i) => 32 | listDep(dependency, i, dependencies), 33 | )} 34 | 35 | ); 36 | }; 37 | 38 | const listDep = (dependency, i, dependencies) => { 39 | const fqn = dependency.namespace + '.' + dependency.name; 40 | const version_range = dependencies[fqn]; 41 | 42 | if (dependency.path) { 43 | return ( 44 | 45 | {fqn}: {version_range} 46 | 47 | ); 48 | } else { 49 | return ( 50 | 51 | {fqn}: {version_range}{' '} 52 | 55 | 56 | ); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/collection-filter.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { AnsibleRepositoryAPI, TagAPI } from 'src/api'; 3 | 4 | const loadRepos = (inputText) => 5 | AnsibleRepositoryAPI.list({ 6 | name__icontains: inputText, 7 | pulp_label_select: '!hide_from_search', 8 | }).then(({ data: { results } }) => 9 | results.map(({ name }) => ({ 10 | id: name, 11 | title: name, 12 | })), 13 | ); 14 | 15 | const loadTags = (inputText) => 16 | TagAPI.listCollections({ 17 | name__icontains: inputText, 18 | sort: '-count', 19 | }).then(({ data: { data } }) => 20 | data.map(({ name, count }) => ({ 21 | id: name, 22 | title: count === undefined ? name : t`${name} (${count})`, 23 | })), 24 | ); 25 | 26 | export const collectionFilter = ({ 27 | featureFlags: { display_signatures, display_repositories }, 28 | ignoredParams: i, 29 | }) => { 30 | const displayNamespaces = !i.includes('namespace'); 31 | const displayRepos = display_repositories && !i.includes('repository_name'); 32 | const displayTags = !i.includes('tags'); 33 | 34 | const filterConfig = [ 35 | { 36 | id: 'keywords', 37 | title: t`Keywords`, 38 | }, 39 | displayRepos && { 40 | id: 'repository_name', 41 | title: t`Repository`, 42 | inputType: 'typeahead' as const, 43 | }, 44 | displayNamespaces && { 45 | id: 'namespace', 46 | title: t`Namespace`, 47 | }, 48 | displayTags && { 49 | id: 'tags', 50 | title: t`Tag`, 51 | inputType: 'typeahead' as const, 52 | }, 53 | display_signatures && { 54 | id: 'is_signed', 55 | title: t`Sign state`, 56 | inputType: 'select' as const, 57 | options: [ 58 | { id: 'true', title: t`Signed` }, 59 | { id: 'false', title: t`Unsigned` }, 60 | ], 61 | }, 62 | ].filter(Boolean); 63 | 64 | const sortOptions = [ 65 | { title: t`Name`, id: 'name', type: 'alpha' as const }, 66 | { title: t`Namespace`, id: 'namespace', type: 'alpha' as const }, 67 | { title: t`Last modified`, id: 'pulp_created', type: 'numeric' as const }, 68 | { title: t`Version`, id: 'version', type: 'numeric' as const }, 69 | ]; 70 | 71 | return { 72 | filterConfig, 73 | sortOptions, 74 | typeaheads: { 75 | repository_name: loadRepos, 76 | tags: loadTags, 77 | }, 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/collection-info.scss: -------------------------------------------------------------------------------- 1 | .install-title { 2 | min-width: 100px; 3 | vertical-align: middle; 4 | display: table-cell; 5 | } 6 | 7 | .pulp-readme-container { 8 | position: relative; 9 | max-height: 300px; 10 | min-height: 50px; 11 | overflow: hidden; 12 | } 13 | 14 | .pulp-fade-out { 15 | position: absolute; 16 | z-index: 1; 17 | bottom: 0; 18 | left: 0; 19 | pointer-events: none; 20 | background-image: linear-gradient( 21 | to bottom, 22 | rgba(255 255 255 / 0%), 23 | rgba(255 255 255 / 100%) 90% 24 | ); 25 | width: 100%; 26 | height: 50px; 27 | } 28 | 29 | .info-panel { 30 | padding: 8px; 31 | } 32 | 33 | .pulp-collection-download-alert { 34 | width: 100%; 35 | } 36 | 37 | .pulp-collection-download-alert > h4 { 38 | margin-top: 0; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/collection-list.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { DataList } from '@patternfly/react-core'; 3 | import { type ReactNode } from 'react'; 4 | import { type CollectionVersionSearch } from 'src/api'; 5 | import { 6 | CollectionListItem, 7 | EmptyStateFilter, 8 | PulpPagination, 9 | } from 'src/components'; 10 | import { ParamHelper } from 'src/utilities'; 11 | 12 | interface IProps { 13 | collectionControls: (collection) => { 14 | dropdownMenu?: ReactNode | null; 15 | uploadButton?: ReactNode | null; 16 | }; 17 | collections: CollectionVersionSearch[]; 18 | displaySignatures: boolean; 19 | ignoredParams: string[]; 20 | itemCount: number; 21 | params: { 22 | page?: number; 23 | page_size?: number; 24 | sort?: string; 25 | }; 26 | updateParams: (params) => void; 27 | } 28 | 29 | // only used in namespace detail, collections uses individual items 30 | export const CollectionList = ({ 31 | collectionControls, 32 | collections, 33 | displaySignatures, 34 | ignoredParams, 35 | itemCount, 36 | params, 37 | updateParams, 38 | }: IProps) => { 39 | return ( 40 | <> 41 | 42 | {collections.length ? ( 43 | collections.map((c, i) => ( 44 | 51 | )) 52 | ) : ( 53 | { 55 | ParamHelper.clearAllFilters({ 56 | params, 57 | ignoredParams, 58 | updateParams, 59 | }); 60 | }} 61 | /> 62 | )} 63 | 64 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/confirm-modal.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Button, Modal } from '@patternfly/react-core'; 3 | import { type ReactNode } from 'react'; 4 | import { Spinner } from 'src/components'; 5 | 6 | interface IProps { 7 | cancelAction: () => void; 8 | children?: ReactNode; 9 | confirmAction?: () => void; 10 | isDisabled?: boolean; 11 | title: string; 12 | spinner?: boolean; 13 | confirmButtonTitle?: string; 14 | } 15 | 16 | export const ConfirmModal = (props: IProps) => { 17 | const { 18 | cancelAction, 19 | children, 20 | confirmAction, 21 | isDisabled, 22 | title, 23 | spinner, 24 | confirmButtonTitle, 25 | } = props; 26 | 27 | return ( 28 | 36 | {confirmButtonTitle ? confirmButtonTitle : t`Yes`} 37 | {spinner && } 38 | , 39 | , 42 | ]} 43 | isOpen 44 | onClose={cancelAction} 45 | title={title} 46 | titleIconVariant='warning' 47 | variant='small' 48 | > 49 | {children} 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/copy-url.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { ClipboardCopy } from 'src/components'; 3 | 4 | export const CopyURL = ({ 5 | url, 6 | fallback = null, 7 | }: { 8 | url: string; 9 | fallback?: true | string; 10 | }) => { 11 | if (fallback === true) { 12 | fallback = t`None`; 13 | } 14 | 15 | return url ? ( 16 | 17 | {url} 18 | 19 | ) : ( 20 | <>{fallback} 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/date-component.tsx: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | import { Tooltip } from 'src/components'; 3 | import { language as locale } from 'src/l10n'; 4 | 5 | const dateFormat = (date) => 6 | DateTime.fromISO(date, { locale }).toFormat('d MMMM y, HH:mm z'); 7 | const relativeFormat = (date) => 8 | DateTime.fromISO(date, { locale }).toRelative(); 9 | 10 | export const DateComponent = ({ date }: { date: string }) => 11 | date && ( 12 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/delete-group-modal.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Trans } from '@lingui/react/macro'; 3 | import { List, ListItem } from '@patternfly/react-core'; 4 | import { type UserType } from 'src/api'; 5 | import { Alert, DeleteModal, Spinner } from 'src/components'; 6 | 7 | interface IProps { 8 | canViewUsers?: boolean; 9 | cancelAction: () => void; 10 | count?: number; 11 | deleteAction: () => void; 12 | name: string; 13 | users?: UserType[]; 14 | } 15 | 16 | export const DeleteGroupModal = ({ 17 | canViewUsers, 18 | cancelAction, 19 | count, 20 | deleteAction, 21 | name, 22 | users, 23 | }: IProps) => ( 24 | 29 | 30 | {name} will be permanently deleted. 31 | 32 |

 

33 |
34 | {users && count > 10 && ( 35 |

{t`Deleting this group will affect ${count} users.`}

36 | )} 37 | {users && count > 0 && count <= 10 && ( 38 | <> 39 |

{t`These users will lose access to the group content:`}

40 | 41 | {users.map((u) => ( 42 | 43 | {u.username} 44 | 45 | ))} 46 | 47 | 48 | )} 49 | 50 | {canViewUsers ? ( 51 | <> 52 | {users && !count &&

{t`No users will be affected.`}

} 53 | {!users && ( 54 |

55 | 56 | Checking for affected users... 57 | 58 |

59 | )} 60 | 61 | ) : ( 62 | 67 | {t`You don't have permission to display users.`} 68 | 69 | )} 70 |
71 |
72 | ); 73 | -------------------------------------------------------------------------------- /src/components/delete-modal.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Button, Modal, type ModalProps } from '@patternfly/react-core'; 3 | import { type ReactNode } from 'react'; 4 | import { Spinner } from 'src/components'; 5 | 6 | interface IProps { 7 | cancelAction: () => void; 8 | children?: ReactNode; 9 | deleteAction: () => void; 10 | isDisabled?: boolean; 11 | isRemove?: boolean; 12 | title: string; 13 | spinner?: boolean; 14 | 'data-cy'?: string; 15 | variant?: ModalProps['variant']; 16 | } 17 | 18 | export const DeleteModal = ({ 19 | cancelAction, 20 | children, 21 | deleteAction, 22 | isDisabled, 23 | isRemove, 24 | title, 25 | spinner, 26 | variant = 'small', 27 | }: IProps) => { 28 | return ( 29 | 32 | 41 |
, 42 | , 45 | ]} 46 | isOpen 47 | onClose={cancelAction} 48 | title={title} 49 | titleIconVariant='warning' 50 | variant={variant} 51 | data-cy='modal_checkbox' 52 | > 53 | {children} 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/delete-remote-modal.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Trans } from '@lingui/react/macro'; 3 | import { Text } from '@patternfly/react-core'; 4 | import { useState } from 'react'; 5 | import { DeleteModal } from 'src/components'; 6 | 7 | interface IProps { 8 | closeAction: () => void; 9 | deleteAction: () => void; 10 | name: string; 11 | } 12 | 13 | export const DeleteRemoteModal = ({ 14 | closeAction, 15 | deleteAction, 16 | name, 17 | }: IProps) => { 18 | const [pending, setPending] = useState(false); 19 | 20 | if (!name) { 21 | return null; 22 | } 23 | 24 | return ( 25 | { 28 | setPending(false); 29 | closeAction(); 30 | }} 31 | deleteAction={() => { 32 | setPending(false); 33 | deleteAction(); 34 | }} 35 | isDisabled={pending} 36 | title={t`Delete remote?`} 37 | > 38 | 39 | 40 | Are you sure you want to delete the remote {name}?
41 | Note: This will also delete all associated resources under this 42 | remote. 43 |
44 |
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/delete-repository-modal.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Trans } from '@lingui/react/macro'; 3 | import { Text } from '@patternfly/react-core'; 4 | import { useState } from 'react'; 5 | import { DeleteModal } from 'src/components'; 6 | 7 | interface IProps { 8 | closeAction: () => void; 9 | deleteAction: () => void; 10 | name: string; 11 | } 12 | 13 | export const DeleteRepositoryModal = ({ 14 | closeAction, 15 | deleteAction, 16 | name, 17 | }: IProps) => { 18 | const [pending, setPending] = useState(false); 19 | 20 | if (!name) { 21 | return null; 22 | } 23 | 24 | return ( 25 | { 28 | setPending(false); 29 | closeAction(); 30 | }} 31 | deleteAction={() => { 32 | setPending(false); 33 | deleteAction(); 34 | }} 35 | isDisabled={pending} 36 | title={t`Delete repository?`} 37 | > 38 | 39 | 40 | Are you sure you want to delete the repository {name}?
41 | Note: This will also delete all associated resources under this 42 | repository. 43 |
44 |
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/deprecated-tag.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | 3 | export const DeprecatedTag = () => ( 4 |
18 | {t`DEPRECATED`} 19 |
20 | ); 21 | -------------------------------------------------------------------------------- /src/components/details.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DescriptionList, 3 | DescriptionListDescription, 4 | DescriptionListGroup, 5 | DescriptionListTerm, 6 | } from '@patternfly/react-core'; 7 | import { type ReactNode } from 'react'; 8 | 9 | interface IProps { 10 | item?: object; 11 | fields?: { 12 | label: string; 13 | value: string | number | boolean | ReactNode; 14 | }[]; 15 | } 16 | 17 | export const Details = ({ item, fields = [] }: IProps) => ( 18 | <> 19 | 20 | {fields.map(({ label, value }) => ( 21 | 22 | {label} 23 | {value} 24 | 25 | ))} 26 | 27 | {item && ( 28 |
29 |         {JSON.stringify(item, null, 2)}
30 |       
31 | )} 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /src/components/download-count.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon'; 3 | import { Tooltip } from 'src/components'; 4 | import { language } from 'src/l10n'; 5 | 6 | interface IProps { 7 | item?: { download_count?: number }; 8 | } 9 | 10 | export const DownloadCount = ({ item }: IProps) => { 11 | if (!item?.download_count) { 12 | return null; 13 | } 14 | 15 | const downloadCount = new Intl.NumberFormat(language).format( 16 | item.download_count, 17 | ); 18 | 19 | return ( 20 | 23 | {t`${downloadCount} downloads`} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/empty-state-custom.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | EmptyState, 3 | EmptyStateActions, 4 | EmptyStateBody, 5 | EmptyStateFooter, 6 | EmptyStateHeader, 7 | EmptyStateIcon, 8 | EmptyStateVariant, 9 | } from '@patternfly/react-core'; 10 | import { type ComponentClass, type ReactElement, type ReactNode } from 'react'; 11 | 12 | interface IProps { 13 | icon?: ComponentClass; 14 | title: string; 15 | description: ReactNode; 16 | button?: ReactElement; 17 | variant?: 'xs' | 'small' | 'large' | 'xl' | 'full'; 18 | } 19 | 20 | export const EmptyStateCustom = ({ 21 | icon, 22 | title, 23 | description, 24 | button, 25 | variant = 'small', 26 | }: IProps) => { 27 | return ( 28 | 29 | {icon ? : null} 30 | {title}} headingLevel='h4' /> 31 | {description} 32 | 33 | {button && {button}} 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/empty-state-filter.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Button } from '@patternfly/react-core'; 3 | import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; 4 | import { EmptyStateCustom } from 'src/components'; 5 | 6 | interface IProps { 7 | clearAllFilters?: () => void; 8 | } 9 | 10 | export const EmptyStateFilter = (props: IProps) => { 11 | return ( 12 | 19 | {t`Clear all filters`} 20 | 21 | ) : null 22 | } 23 | /> 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/empty-state-no-data.tsx: -------------------------------------------------------------------------------- 1 | import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon'; 2 | import PlusCircleIcon from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; 3 | import { type ReactElement, type ReactNode } from 'react'; 4 | import { EmptyStateCustom } from 'src/components'; 5 | 6 | interface IProps { 7 | button?: ReactElement; 8 | title: string; 9 | description: ReactNode; 10 | } 11 | 12 | export const EmptyStateNoData = (props: IProps) => { 13 | return ( 14 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/empty-state-unauthorized.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import LockIcon from '@patternfly/react-icons/dist/esm/icons/lock-icon'; 3 | import { EmptyStateCustom, LoginLink } from 'src/components'; 4 | 5 | export const EmptyStateUnauthorized = () => { 6 | return ( 7 | 12 | {t`Contact your organization administrator for more information.`} 13 |
14 |
15 | 16 | 17 | } 18 | /> 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/empty-state-under-construction.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import WrenchIcon from '@patternfly/react-icons//dist/esm/icons/wrench-icon'; 3 | import { EmptyStateCustom } from 'src/components'; 4 | 5 | // TODO add link to Pulp page 6 | export const EmptyStateNotImplemented = () => { 7 | return ( 8 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/empty-state-xs.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactElement, type ReactNode } from 'react'; 2 | import { EmptyStateCustom } from 'src/components'; 3 | 4 | interface IProps { 5 | button?: ReactElement; 6 | title: string; 7 | description: ReactNode; 8 | } 9 | 10 | export const EmptyStateXs = (props: IProps) => { 11 | return ( 12 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import { isRouteErrorResponse, useRouteError } from 'react-router'; 2 | 3 | export const ErrorBoundary = () => { 4 | const error = useRouteError(); 5 | 6 | if (isRouteErrorResponse(error)) { 7 | return ( 8 | <> 9 |

10 | {error.status} {error.statusText} 11 |

12 |

{error.data.toString()}

13 | 14 | ); 15 | } else if (error instanceof Error) { 16 | return ( 17 |
18 |

Error

19 |

{error.message}

20 |

The stack trace is:

21 |
{error.stack}
22 |
23 | ); 24 | console.error(error); 25 | return
Something went horribly wrong!
; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; 2 | import { type ReactNode } from 'react'; 3 | 4 | interface IProps { 5 | children: ReactNode; 6 | 'data-cy'?: string; 7 | href: string; 8 | variant?: 'default' | 'download' | 'menu' | 'nav'; 9 | } 10 | 11 | // variants: 12 | // download - no external link icon (role download) 13 | // menu - top nav question mark menu (Customer Support, Training) 14 | // nav - left side nav (Documentation, Terms of Use) 15 | // default - everywhere else 16 | 17 | export const ExternalLink = ({ 18 | children, 19 | 'data-cy': dataCy, 20 | href, 21 | variant = 'default', 22 | }: IProps) => { 23 | if (!href || !children) { 24 | return null; 25 | } 26 | 27 | const iconStyle: Record = { 28 | nav: { position: 'absolute', right: '32px', top: '22px' }, 29 | download: { display: 'none' }, 30 | }[variant]; 31 | const className = { 32 | nav: 'pf-v5-c-nav__link', 33 | menu: 'pf-v5-c-dropdown__menu-item', 34 | }[variant]; 35 | 36 | return ( 37 | 44 | {children}{' '} 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/form-field-helper.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormHelperText, 3 | HelperText, 4 | HelperTextItem, 5 | type HelperTextItemProps, 6 | } from '@patternfly/react-core'; 7 | import { type ReactNode } from 'react'; 8 | 9 | export function FormFieldHelper({ 10 | variant = 'default', 11 | children = null, 12 | }: { 13 | variant?: string; // default | indeterminate | success | error | warning 14 | children?: ReactNode; 15 | }) { 16 | return ( 17 | 18 | 19 | 23 | {children} 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/group-modal.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { 3 | Button, 4 | Form, 5 | FormGroup, 6 | Modal, 7 | ModalVariant, 8 | TextInput, 9 | } from '@patternfly/react-core'; 10 | import { useState } from 'react'; 11 | import { FormFieldHelper } from 'src/components'; 12 | import { type ErrorMessagesType } from 'src/utilities'; 13 | 14 | interface IProps { 15 | clearErrors?: () => void; 16 | errorMessage?: ErrorMessagesType; 17 | group?: { name: string }; 18 | onCancel?: () => void; 19 | onSave?: (string) => void; 20 | } 21 | 22 | export const GroupModal = ({ 23 | clearErrors, 24 | errorMessage, 25 | group, 26 | onCancel, 27 | onSave, 28 | }: IProps) => { 29 | const [name, setName] = useState(group?.name || ''); 30 | 31 | return ( 32 | { 35 | onCancel(); 36 | }} 37 | isOpen 38 | title={t`Create a group`} 39 | actions={[ 40 |
41 | 49 |
, 50 | , 53 | ]} 54 | > 55 |
{ 57 | e.preventDefault(); 58 | onSave(name); 59 | }} 60 | > 61 | 62 | { 66 | setName(value); 67 | clearErrors(); 68 | }} 69 | type='text' 70 | validated={errorMessage ? 'error' : 'default'} 71 | /> 72 | 73 | {errorMessage?.name} 74 | 75 | 76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/header.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-md: 1000px; 2 | 3 | .pulp-header-bordered { 4 | border-bottom: 1px solid #d8d8d8; 5 | } 6 | 7 | .pulp-header-image { 8 | padding-right: 16px; 9 | } 10 | 11 | .pulp-header-bottom { 12 | padding-top: 10px; 13 | } 14 | 15 | .pulp-tab-link-container { 16 | display: flex; 17 | margin-top: 10px; 18 | 19 | @media (max-width: $breakpoint-md) { 20 | flex-direction: column-reverse; 21 | } 22 | 23 | .tabs { 24 | flex-grow: 0; 25 | } 26 | 27 | // Tabs isDisabled 28 | .pf-v5-c-tabs__item.disabled .pf-v5-c-tabs__link { 29 | cursor: not-allowed; 30 | } 31 | 32 | .links { 33 | flex-grow: 1; 34 | display: flex; 35 | align-items: center; 36 | flex-wrap: wrap; 37 | 38 | @media (min-width: $breakpoint-md) { 39 | justify-content: flex-end; 40 | } 41 | 42 | @media (max-width: $breakpoint-md) { 43 | margin-bottom: 10px; 44 | } 45 | 46 | div { 47 | @media (min-width: $breakpoint-md) { 48 | margin-left: 20px; 49 | } 50 | 51 | @media (max-width: $breakpoint-md) { 52 | margin-right: 20px; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/help-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button, PopoverPosition } from '@patternfly/react-core'; 2 | import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon'; 3 | import { type ReactNode } from 'react'; 4 | import { Popover } from 'src/components'; 5 | 6 | interface IProps { 7 | /** Value to display in the tag */ 8 | content: ReactNode; 9 | hasAutoWidth?: boolean; 10 | header?: ReactNode; 11 | prefix?: ReactNode; 12 | } 13 | 14 | export const HelpButton = ({ 15 | content, 16 | hasAutoWidth, 17 | header, 18 | prefix, 19 | }: IProps) => ( 20 | 26 | 35 | 36 | ); 37 | -------------------------------------------------------------------------------- /src/components/import-modal.scss: -------------------------------------------------------------------------------- 1 | $charcoal: #808080; 2 | $green: #5bb75b; 3 | 4 | .upload-collection { 5 | .file-error-messages { 6 | color: var(--pf-v5-global--danger-color--100); 7 | 8 | &.skipped { 9 | color: var(--pf-v5-global--warning-color--100); 10 | } 11 | } 12 | 13 | .upload-file { 14 | width: 0.1px; 15 | height: 0.1px; 16 | opacity: 0; 17 | overflow: hidden; 18 | position: absolute; 19 | z-index: -1; 20 | } 21 | 22 | .upload-file-label { 23 | display: inline-block; 24 | cursor: pointer; 25 | width: 100%; 26 | font-weight: normal; 27 | 28 | .upload-box { 29 | display: flex; 30 | border-style: solid; 31 | border-width: thin; 32 | width: 100%; 33 | border-radius: 3px; 34 | 35 | .upload-button, 36 | .upload-text { 37 | padding: 5px; 38 | } 39 | 40 | .upload-button { 41 | border-right: thin solid; 42 | padding-left: 10px; 43 | padding-right: 10px; 44 | } 45 | 46 | .upload-button:hover { 47 | color: white; 48 | background-color: $charcoal; 49 | } 50 | 51 | .upload-text { 52 | position: relative; 53 | width: 100%; 54 | } 55 | 56 | .loading-bar { 57 | position: absolute; 58 | height: 3px; 59 | background-color: $green; 60 | bottom: 0; 61 | left: 0; 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/language-switcher.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { 3 | DropdownItem, 4 | DropdownSeparator, 5 | } from '@patternfly/react-core/deprecated'; 6 | import { StatefulDropdown } from 'src/components'; 7 | import { availableLanguages, language, languageNames } from 'src/l10n'; 8 | 9 | export function LanguageSwitcher(_props) { 10 | const currentLanguage = languageNames[language] || language; 11 | 12 | return ( 13 | 20 | {window.localStorage.override_l10n 21 | ? t`${currentLanguage} (current)` 22 | : t`${currentLanguage} (browser default)`} 23 | , 24 | , 25 | ...availableLanguages.map((lang) => ( 26 | 31 | {languageNames[lang] || lang} 32 | 33 | )), 34 | , 35 | 40 | {t`Reset to browser defaults`} 41 | , 42 | ]} 43 | /> 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/link-tabs.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames'; 2 | import { type ReactNode } from 'react'; 3 | import { Link } from 'react-router'; 4 | 5 | export interface LinkTabsProps { 6 | /** List of tabs */ 7 | tabs: { 8 | active?: boolean; 9 | link: string; 10 | title: string; 11 | icon?: ReactNode; 12 | }[]; 13 | } 14 | 15 | const renderTab = ({ link, title, icon = null, active = false }) => ( 16 |
  • 23 | 24 | 25 | {icon ? <>{icon} : null} 26 | {title} 27 | 28 | 29 |
  • 30 | ); 31 | 32 | // We're not using the Tab react component because they don't support links. 33 | export const LinkTabs = ({ tabs }: LinkTabsProps) => ( 34 |
    35 |
      36 | {tabs.filter(Boolean).map((tab) => renderTab(tab))} 37 |
    38 |
    39 | ); 40 | -------------------------------------------------------------------------------- /src/components/list-item-actions.tsx: -------------------------------------------------------------------------------- 1 | import { List } from '@patternfly/react-core'; 2 | import { Td } from '@patternfly/react-table'; 3 | import { type ReactNode } from 'react'; 4 | import { StatefulDropdown } from 'src/components'; 5 | 6 | interface IProps { 7 | kebabItems?: ReactNode[]; 8 | buttons?: ReactNode[]; 9 | } 10 | 11 | export function ListItemActions(props: IProps) { 12 | const buttons = props.buttons?.filter(Boolean); 13 | const kebabItems = props.kebabItems?.filter(Boolean); 14 | const anyButtons = buttons?.length; 15 | const anyKebab = kebabItems?.length; 16 | 17 | return ( 18 | 26 | {anyButtons ? ( 27 | <> 28 | {buttons}{' '} 29 | 30 | ) : null} 31 | {anyKebab ? ( 32 |
    33 | {' '} 34 |
    35 | ) : null} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/list-item.scss: -------------------------------------------------------------------------------- 1 | .pulp-entry { 2 | margin-top: 5px; 3 | 4 | &.pulp-right-col { 5 | width: 200px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/loading-page.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton, Title } from '@patternfly/react-core'; 2 | import { LoadingSpinner, Main } from 'src/components'; 3 | 4 | export const LoadingPage = (_props) => ( 5 | <> 6 |
    9 | 10 | <Skeleton /> 11 | 12 |
    13 |
    14 |
    15 | 16 |
    17 |
    18 | 19 | ); 20 | -------------------------------------------------------------------------------- /src/components/loading-spinner.tsx: -------------------------------------------------------------------------------- 1 | import { Bullseye } from '@patternfly/react-core'; 2 | import { Spinner } from 'src/components'; 3 | 4 | export const LoadingSpinner = () => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/login-link.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Link, useLocation } from 'react-router'; 3 | import { Paths, formatPath } from 'src/paths'; 4 | import { config } from 'src/ui-config'; 5 | import { loginURL } from 'src/utilities'; 6 | 7 | interface IProps { 8 | button?: boolean; 9 | } 10 | 11 | export const LoginLink = ({ button }: IProps) => { 12 | const { pathname } = useLocation(); 13 | const className = button ? 'pf-v5-c-button pf-m-primary' : ''; 14 | 15 | // NOTE: also update AuthHandler#render (src/routes.tsx) when changing this 16 | if (config.UI_EXTERNAL_LOGIN_URI) { 17 | return {t`Login`}; 18 | } 19 | 20 | return ( 21 | {t`Login`} 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import PulpLogo from 'static/images/pulp_logo.png'; 3 | 4 | interface IProps { 5 | alt: string; 6 | className?: string; 7 | fallbackToDefault?: boolean; 8 | flexGrow?: boolean; 9 | image: string; 10 | // size should be css length measurment: '100px' 11 | size: string; 12 | unlockWidth?: boolean; 13 | width?: string; 14 | } 15 | 16 | export const Logo = ({ 17 | alt, 18 | className, 19 | fallbackToDefault, 20 | flexGrow, 21 | image, 22 | size, 23 | unlockWidth, 24 | width, 25 | }: IProps) => { 26 | const [failed, setFailed] = useState(false); 27 | 28 | const style = { 29 | height: size, 30 | display: 'flex', 31 | justifyContent: 'center', 32 | alignItems: 'center', 33 | width, 34 | }; 35 | 36 | if (flexGrow) { 37 | style['flexGrow'] = 1; 38 | } 39 | 40 | if (unlockWidth) { 41 | style['minWidth'] = size; 42 | } else { 43 | style['width'] = size; 44 | } 45 | 46 | // use inline css so we can set size 47 | return ( 48 |
    49 | {alt} setFailed(true) : () => null} 54 | /> 55 |
    56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/main.tsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames'; 2 | import { type HTMLProps, type ReactNode } from 'react'; 3 | 4 | interface IProps extends HTMLProps { 5 | children: ReactNode; 6 | className?: string; 7 | } 8 | 9 | export const Main = ({ children, className, ...extra }: IProps) => ( 10 |
    11 | {children} 12 |
    13 | ); 14 | -------------------------------------------------------------------------------- /src/components/markdown-editor.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-md: 1000px; 2 | 3 | .markdown-editor { 4 | display: flex; 5 | 6 | @media (max-width: $breakpoint-md) { 7 | flex-direction: column-reverse; 8 | } 9 | 10 | .column { 11 | flex-grow: 1; 12 | margin-right: 15px; 13 | margin-bottom: 15px; 14 | } 15 | 16 | .preview-container { 17 | .preview { 18 | overflow: scroll; 19 | height: 500px; 20 | border: 1px solid #ededed; 21 | padding: 5px; 22 | } 23 | } 24 | 25 | .editor { 26 | min-width: 500px; 27 | max-width: 500px; 28 | } 29 | } 30 | 31 | #resources { 32 | height: 500px; 33 | resize: none; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/markdown-editor.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { Form, FormGroup, TextArea } from '@patternfly/react-core'; 3 | import ReactMarkdown from 'react-markdown'; 4 | import { FormFieldHelper } from 'src/components'; 5 | import './markdown-editor.scss'; 6 | 7 | interface IProps { 8 | editing: boolean; 9 | helperText: string; 10 | placeholder: string; 11 | text: string; 12 | updateText: (value) => void; 13 | } 14 | 15 | export const MarkdownEditor = ({ 16 | editing, 17 | helperText, 18 | placeholder, 19 | text, 20 | updateText, 21 | }: IProps) => ( 22 |
    23 |
    24 | {editing && ( 25 |
    26 | 27 |
    {t`Raw Markdown`}
    28 |