├── .babelrc.json ├── .cockpit-ci └── container ├── .eslintrc.json ├── .flake8 ├── .fmf └── version ├── .github └── workflows │ ├── automerge-deps.yml │ ├── check-flake8.yml │ ├── check-lint.yml │ ├── check-rpm.yml │ ├── check-translations.yml │ ├── coverity.yml │ ├── pr_best_practices.yml │ ├── release.yml │ └── stale-cleanup.yml ├── .gitignore ├── Dockerfile.buildrpm ├── LICENSE.txt ├── Makefile ├── README.md ├── cockpit-composer.spec.in ├── package-lock.json ├── package.json ├── packit.yaml ├── plans └── all.fmf ├── public ├── index.html ├── manifest.json └── org.image-builder.cockpit-composer.metainfo.xml ├── rpmversion.sh ├── src ├── App.css ├── App.js ├── api.js ├── components │ ├── Cards │ │ └── UserCard.js │ ├── EmptyStates │ │ ├── APIEmpty.js │ │ ├── BlueprintsEmpty.js │ │ ├── ImagesEmpty.js │ │ └── PackagesEmpty.js │ ├── Modal │ │ ├── DeleteBlueprint.js │ │ ├── DeleteImage.js │ │ ├── ExportBlueprint.css │ │ ├── ExportBlueprint.js │ │ ├── FDOCard.js │ │ ├── FilesystemCard.js │ │ ├── FirewallCard.js │ │ ├── GroupsCard.js │ │ ├── ImportBlueprint.css │ │ ├── ImportBlueprint.js │ │ ├── KernelCard.js │ │ ├── LocaleCard.js │ │ ├── OpenSCAPCard.js │ │ ├── OtherCard.js │ │ ├── SSHKeysCard.js │ │ ├── ServicesCard.js │ │ ├── SourceCardModal.js │ │ ├── StopBuild.js │ │ ├── TimezoneCard.js │ │ └── ignitionCard.js │ ├── Notifications │ │ └── Notifications.js │ ├── Tab │ │ ├── CustomizationsTab.js │ │ ├── ImagesTab.js │ │ ├── PackagesTab.js │ │ └── SourcesTab.js │ ├── Table │ │ ├── BlueprintTable.js │ │ ├── ImageTable.js │ │ └── PackagesTable.js │ ├── Toolbar │ │ ├── BlueprintDetailsToolbar.js │ │ ├── BlueprintToolbar.js │ │ └── PackagesToolbar.js │ └── Wizard │ │ ├── BlueprintWizard.js │ │ ├── CreateImageWizard.css │ │ └── CreateImageWizard.js ├── constants.js ├── forms │ ├── components │ │ ├── BlueprintSelect.js │ │ ├── FileSystemConfigButtons.js │ │ ├── FileSystemConfigToggle.js │ │ ├── FileSystemConfiguration.js │ │ ├── FormGroupCustom.js │ │ ├── ImageOutputSelect.js │ │ ├── MountPoint.js │ │ ├── Packages.js │ │ ├── SizeUnit.js │ │ ├── SubmitButtonsCustom.js │ │ ├── TextFieldCustom.js │ │ ├── TextInputGroupWithChips.js │ │ ├── UploadFile.js │ │ └── UploadOCIFile.js │ ├── schemas │ │ ├── fdo.js │ │ ├── filesystem.js │ │ ├── firewall.js │ │ ├── groups.js │ │ ├── ignition.js │ │ ├── kernel.js │ │ ├── locale.js │ │ ├── openscap.js │ │ ├── other.js │ │ ├── services.js │ │ ├── sshkeys.js │ │ └── timezone.js │ ├── steps │ │ ├── awsAuth.js │ │ ├── awsDest.js │ │ ├── azureAuth.js │ │ ├── azureDest.js │ │ ├── blueprintDetails.js │ │ ├── fdo.js │ │ ├── filesystem.js │ │ ├── firewall.js │ │ ├── gcp.js │ │ ├── groups.js │ │ ├── ignition.js │ │ ├── imageOutput.js │ │ ├── imageOutputStepMapper.js │ │ ├── index.js │ │ ├── kernel.js │ │ ├── locale.js │ │ ├── ociAuth.js │ │ ├── ociDest.js │ │ ├── openscap.js │ │ ├── ostreeSettings.js │ │ ├── other.js │ │ ├── packages.js │ │ ├── reviewBlueprint.js │ │ ├── reviewImage.js │ │ ├── services.js │ │ ├── sshkeys.js │ │ ├── timezone.js │ │ ├── users.js │ │ ├── vmwareAuth.js │ │ └── vmwareDest.js │ └── validators │ │ ├── blueprintNameValidator.js │ │ ├── filesystemValidator.js │ │ ├── hostnameValidator.js │ │ ├── index.js │ │ └── ostreeValidator.js ├── helpers.js ├── pages │ ├── blueprintDetails.css │ ├── blueprintDetails.js │ └── blueprintList.js ├── slices │ ├── alertsSlice.js │ ├── blueprintsSlice.js │ ├── imagesSlice.js │ └── sourcesSlice.js └── store.js ├── test ├── README.md ├── browser │ ├── browser.sh │ ├── main.fmf │ └── run-tests.sh ├── files │ ├── httpd-server-with-hostname.toml │ ├── httpd-server-with-user.toml │ ├── httpd-server.toml │ ├── openssh-server.toml │ ├── rhel-10.json │ ├── rhel-9.6.json │ └── rhel-95.json ├── osbuild-mock.repo ├── reference-image ├── run ├── verify │ ├── check-blueprintList │ ├── check-blueprintWizard │ ├── check-imageWizard │ ├── composerlib.py │ └── parent.py └── vm.install ├── translations ├── cs.json ├── de.json ├── en.json ├── fr.json ├── ja.json ├── ka.json ├── ko.json ├── tr.json ├── uk.json └── zh_CN.json └── webpack.config.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | [ 8 | "formatjs", 9 | { 10 | "idInterpolationPattern": "[sha512:contenthash:base64:6]", 11 | "ast": true 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.cockpit-ci/container: -------------------------------------------------------------------------------- 1 | ghcr.io/cockpit-project/tasks:2024-08-19 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:prettier/recommended" 11 | ], 12 | "overrides": [ 13 | ], 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react", 20 | "prettier" 21 | ], 22 | "rules": { 23 | "prettier/prettier": "error" 24 | }, 25 | "settings": { 26 | "react": { 27 | "version": "detect" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Specify the number of subprocesses that Flake8 will use to run checks in parallel. 3 | jobs = auto 4 | # Print the total number of errors. 5 | count = True 6 | # Print the source code generating the error/warning in question. 7 | show-source = True 8 | # Count the number of occurrences of each error/warning code and print a report. 9 | statistics = True 10 | 11 | exclude = 12 | # No need to traverse our git directory 13 | .git, 14 | # There's no value in checking cache directories 15 | __pycache__, 16 | bots, 17 | test/common 18 | 19 | # Set the maximum length that any line (with some exceptions) may be. 20 | max-line-length = 100 21 | # Set the maximum allowed McCabe complexity value for a block of code. 22 | max-complexity = 10 23 | 24 | # ERROR CODES 25 | # 26 | # E/W - PEP8 errors/warnings (pycodestyle) 27 | # F - linting errors (pyflakes) 28 | # C - McCabe complexity error (mccabe) 29 | # 30 | # F401 - Module imported but unused 31 | # C901 - Function is too complex 32 | # E501 - Line too long > 80 characters 33 | 34 | # Specify a list of codes to ignore. 35 | ignore = 36 | C901, 37 | E501 38 | 39 | # Per file ignore 40 | per-file-ignores = 41 | test/verify/composerlib.py:F401 42 | test/verify/parent.py:E501 43 | 44 | # Specify the list of error codes you wish Flake8 to report. 45 | select = 46 | E, 47 | W, 48 | F, 49 | C 50 | -------------------------------------------------------------------------------- /.fmf/version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /.github/workflows/automerge-deps.yml: -------------------------------------------------------------------------------- 1 | name: Auto merge dependabot PRs 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.1.1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --rebase "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | - name: Approve a PR 24 | run: gh pr review --approve "$PR_URL" 25 | env: 26 | PR_URL: ${{github.event.pull_request.html_url}} 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | -------------------------------------------------------------------------------- /.github/workflows/check-flake8.yml: -------------------------------------------------------------------------------- 1 | name: Check flake8 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | flake8: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Setup Python 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: '3.x' 15 | 16 | - name: Lint with flake8 17 | run: | 18 | pip install flake8 19 | make flake8 20 | -------------------------------------------------------------------------------- /.github/workflows/check-lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '16.x' 14 | 15 | - name: Install Dependencies 16 | run: npm ci 17 | 18 | - name: Run test 19 | run: make lint 20 | -------------------------------------------------------------------------------- /.github/workflows/check-rpm.yml: -------------------------------------------------------------------------------- 1 | name: Check rpm builds 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build-rpm: 7 | runs-on: ubuntu-latest 8 | container: 9 | image: fedora:36 10 | steps: 11 | - name: Install dependencies 12 | run: | 13 | dnf install -y make rpm-build jq git libappstream-glib 14 | 15 | - name: Clone repository 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Install npm 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '16.x' 24 | 25 | # HACK: NPM doesn't like to run in the mounted $HOME volume 26 | - name: Build release 27 | run: | 28 | mkdir -p /tmp/checkout 29 | cp -r . /tmp/checkout 30 | cd /tmp/checkout 31 | make rpm srpm 32 | -------------------------------------------------------------------------------- /.github/workflows/check-translations.yml: -------------------------------------------------------------------------------- 1 | name: Check translations source 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check-translations: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '16.x' 13 | - name: Check source file has been generated 14 | run: | 15 | npm i 16 | npm run translations:extract 17 | if [ -n "$(git status --porcelain)" ]; then 18 | echo 19 | echo "translations/en.json needs to be generated" 20 | echo "Please run `npm run translations:extract`" 21 | exit "1" 22 | else 23 | exit "0" 24 | fi 25 | -------------------------------------------------------------------------------- /.github/workflows/coverity.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Coverity 3 | on: 4 | schedule: 5 | - cron: '0 5 * * *' # Daily at 05:00 UTC 6 | 7 | jobs: 8 | coverity: 9 | name: "Test Suite" 10 | runs-on: ubuntu-20.04 11 | defaults: 12 | run: 13 | working-directory: cockpit-composer 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v2 17 | with: 18 | path: cockpit-composer 19 | 20 | - name: Install Dependencies 21 | run: | 22 | sudo apt update 23 | sudo apt install nodejs npm 24 | 25 | - name: Download Coverity Tool 26 | run: | 27 | make coverity-download 28 | env: 29 | COVERITY_TOKEN: ${{ secrets.COVERITY_TOKEN }} 30 | 31 | - name: Coverity check 32 | run: | 33 | make coverity-check 34 | 35 | - name: Upload analysis results 36 | run: | 37 | make coverity-submit 38 | env: 39 | COVERITY_TOKEN: ${{ secrets.COVERITY_TOKEN }} 40 | COVERITY_EMAIL: ${{ secrets.COVERITY_EMAIL }} 41 | -------------------------------------------------------------------------------- /.github/workflows/pr_best_practices.yml: -------------------------------------------------------------------------------- 1 | name: "Verify PR best practices" 2 | 3 | on: 4 | pull_request_target: 5 | branches: [ main ] 6 | 7 | jobs: 8 | pr-best-practices: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: PR best practice check 12 | uses: osbuild/pr-best-practices@main 13 | with: 14 | token: ${{ secrets.SCHUTZBOT_GITHUB_ACCESS_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | # this is a glob, not a regexp 6 | - '[0-9]*' 7 | jobs: 8 | source: 9 | runs-on: ubuntu-latest 10 | container: 11 | image: ghcr.io/cockpit-project/tasks:latest 12 | options: --user root 13 | permissions: 14 | # create GitHub release 15 | contents: write 16 | steps: 17 | - name: Clone repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Workaround for https://github.com/actions/checkout/pull/697 23 | run: | 24 | git config --global --add safe.directory /__w/cockpit-composer/cockpit-composer 25 | git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags) 26 | 27 | # HACK: the clean plugin doesn't like to build in the mounted $HOME volume 28 | - name: Build release 29 | run: | 30 | orig=$(pwd) 31 | cp -r . /tmp/checkout 32 | cd /tmp/checkout 33 | make dist-gzip 34 | cp cockpit-composer-*.tar.gz $orig 35 | 36 | - name: Publish GitHub release 37 | uses: cockpit-project/action-release@88d994da62d1451c7073e26748c18413fcdf46e9 38 | with: 39 | filename: "cockpit-composer-${{ github.ref_name }}.tar.gz" 40 | -------------------------------------------------------------------------------- /.github/workflows/stale-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: Mark and close stale issues and PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 4 * * *' 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: osbuild/common-stale-action@main 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bots 2 | node_modules 3 | build 4 | 5 | public/main.js 6 | public/main.css 7 | public/main.js.map 8 | public/main.js.LICENSE.txt 9 | public/main.css.map 10 | 11 | test/images 12 | test/common 13 | test/bots 14 | test/verify/__pycache__ 15 | 16 | translations/compiled 17 | 18 | cockpit-composer.spec 19 | *.tar.gz 20 | *.rpm 21 | *FAIL* 22 | -------------------------------------------------------------------------------- /Dockerfile.buildrpm: -------------------------------------------------------------------------------- 1 | FROM fedora:latest 2 | LABEL maintainer="Xiaofeng Wang" \ 3 | email="xiaofwan@redhat.com" \ 4 | baseimage="Fedora:latest" \ 5 | description="A cockpit-composer RPM builder container running on Fedora" 6 | 7 | RUN dnf install -y make cmake rpm-build which gnupg git tar xz rsync curl jq nodejs python gcc gcc-c++ libappstream-glib && dnf clean all 8 | 9 | WORKDIR /composer 10 | CMD ["make", "rpm", "srpm"] 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /cockpit-composer.spec.in: -------------------------------------------------------------------------------- 1 | Name: cockpit-composer 2 | Version: @VERSION@ 3 | Release: @RELEASE@ 4 | Summary: Composer GUI for use with Cockpit 5 | 6 | License: MIT 7 | URL: http://weldr.io/ 8 | Source0: https://github.com/osbuild/cockpit-composer/releases/download/%{version}/cockpit-composer-%{version}.tar.gz 9 | 10 | BuildArch: noarch 11 | BuildRequires: libappstream-glib 12 | 13 | Requires: cockpit 14 | %if 0%{?fedora} >= 33 || 0%{?rhel} >= 8 15 | Requires: osbuild-composer >= 28 16 | %else 17 | Requires: weldr 18 | Suggests: osbuild-composer >= 28 19 | %endif 20 | 21 | %description 22 | Composer generates custom images suitable for deploying systems or uploading to 23 | the cloud. It integrates into Cockpit as a frontend for osbuild. 24 | 25 | %prep 26 | %setup -q -n cockpit-composer 27 | 28 | %build 29 | # Nothing to build 30 | 31 | %install 32 | mkdir -p %{buildroot}/%{_datadir}/cockpit/composer 33 | cp -a public/* %{buildroot}/%{_datadir}/cockpit/composer 34 | mkdir -p %{buildroot}/%{_datadir}/metainfo/ 35 | appstream-util validate-relax --nonet public/org.image-builder.cockpit-composer.metainfo.xml 36 | cp -a public/org.image-builder.cockpit-composer.metainfo.xml %{buildroot}/%{_datadir}/metainfo/ 37 | 38 | %files 39 | %doc README.md 40 | %license LICENSE.txt 41 | %{_datadir}/cockpit/composer 42 | %{_datadir}/metainfo/* 43 | 44 | %changelog 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cockpit-composer", 3 | "type": "module", 4 | "private": true, 5 | "engines": { 6 | "node": ">=6", 7 | "npm": ">=5.0" 8 | }, 9 | "dependencies": { 10 | "@data-driven-forms/pf4-component-mapper": "3.20.9", 11 | "@data-driven-forms/react-form-renderer": "3.20.9", 12 | "@patternfly/patternfly": "4.224.2", 13 | "@patternfly/react-core": "4.267.6", 14 | "@patternfly/react-icons": "4.93.6", 15 | "@patternfly/react-table": "4.113.0", 16 | "@reduxjs/toolkit": "1.9.5", 17 | "prop-types": "15.8.1", 18 | "react": "17.0.2", 19 | "react-dom": "17.0.2", 20 | "react-intl": "6.6.2", 21 | "react-redux": "8.1.2", 22 | "react-router-dom": "6.22.2", 23 | "uuid": "9.0.1" 24 | }, 25 | "devDependencies": { 26 | "@babel/cli": "7.23.9", 27 | "@babel/core": "7.24.0", 28 | "@babel/preset-env": "7.24.6", 29 | "@babel/preset-react": "7.23.3", 30 | "@formatjs/cli": "6.1.3", 31 | "babel-loader": "9.1.3", 32 | "babel-plugin-formatjs": "10.5.13", 33 | "css-loader": "6.10.0", 34 | "eslint": "8.57.0", 35 | "eslint-config-prettier": "9.1.0", 36 | "eslint-plugin-prettier": "4.2.1", 37 | "eslint-plugin-react": "7.33.2", 38 | "glob": "11.0.0", 39 | "mini-css-extract-plugin": "2.8.0", 40 | "prettier": "2.8.8", 41 | "sass": "1.77.4", 42 | "sass-loader": "14.1.1", 43 | "sizzle": "2.3.10", 44 | "webpack": "5.90.3", 45 | "webpack-cli": "5.0.2" 46 | }, 47 | "scripts": { 48 | "lint": "eslint src", 49 | "lint:fix": "npm run lint --fix", 50 | "prettier": "prettier src --check", 51 | "prettier:fix": "npm run prettier -- --write", 52 | "format": "npm run prettier:fix && npm run lint:fix", 53 | "translations:extract": "formatjs extract 'src/**/*.js' --out-file translations/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]' --format simple", 54 | "translations:compile": "formatjs compile-folder translations translations/compiled --ast --format simple", 55 | "build:dev": "webpack --config webpack.config.js", 56 | "build:prod": "npm run translations:extract && npm run translations:compile && npm run build:dev", 57 | "watch": "npm run build:dev -- --watch" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packit.yaml: -------------------------------------------------------------------------------- 1 | upstream_project_url: https://github.com/osbuild/cockpit-composer 2 | specfile_path: cockpit-composer.spec 3 | upstream_package_name: cockpit-composer 4 | downstream_package_name: cockpit-composer 5 | # use the nicely formatted release description from our upstream release, instead of git shortlog 6 | copy_upstream_release_description: true 7 | 8 | actions: 9 | post-upstream-clone: make spec 10 | create-archive: make dist-gzip 11 | 12 | srpm_build_deps: 13 | - make 14 | - npm 15 | 16 | jobs: 17 | - job: copr_build 18 | trigger: pull_request 19 | targets: &build_targets 20 | - centos-stream-9 21 | - centos-stream-10 22 | - fedora-all 23 | 24 | - job: tests 25 | trigger: pull_request 26 | targets: 27 | - fedora-39 28 | - fedora-39-aarch64 29 | - fedora-40 30 | - fedora-40-aarch64 31 | - centos-stream-9 32 | - centos-stream-9-aarch64 33 | - centos-stream-10 34 | 35 | - job: copr_build 36 | trigger: commit 37 | branch: "^main$" 38 | owner: "@osbuild" 39 | project: "cockpit-composer-main" 40 | preserve_project: True 41 | targets: *build_targets 42 | 43 | - job: copr_build 44 | trigger: release 45 | owner: "@osbuild" 46 | project: "cockpit-composer" 47 | preserve_project: True 48 | targets: *build_targets 49 | actions: 50 | # same as the global one, but specifying actions: does not inherit 51 | post-upstream-clone: make spec 52 | create-archive: 53 | - sh -exc "curl -L -O https://github.com/osbuild/cockpit-composer/releases/download/${PACKIT_PROJECT_VERSION}/${PACKIT_PROJECT_NAME_VERSION}.tar.gz" 54 | - sh -exc "ls ${PACKIT_PROJECT_NAME_VERSION}.tar.gz" 55 | 56 | - job: propose_downstream 57 | trigger: release 58 | dist_git_branches: 59 | - fedora-all 60 | 61 | - job: koji_build 62 | trigger: commit 63 | dist_git_branches: 64 | - fedora-all 65 | 66 | - job: bodhi_update 67 | trigger: commit 68 | dist_git_branches: 69 | # rawhide updates are created automatically 70 | - fedora-branched 71 | -------------------------------------------------------------------------------- /plans/all.fmf: -------------------------------------------------------------------------------- 1 | summary: 2 | Run all tests 3 | discover: 4 | how: fmf 5 | execute: 6 | how: tmt 7 | 8 | # Let's handle them upstream only, don't break Fedora/RHEL reverse dependency gating 9 | environment: 10 | TEST_AUDIT_NO_SELINUX: 1 11 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cockpit-Composer 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": { 3 | "cockpit": "134" 4 | }, 5 | 6 | "dashboard": { 7 | "index": { 8 | "label": "Image Builder", 9 | "order": 30, 10 | "icon": "pficon-build", 11 | "docs": [ 12 | { 13 | "label": "Creating system images", 14 | "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/composing_a_customized_rhel_system_image/creating-system-images-with-composer-web-console-interface_composing-a-customized-rhel-system-image" 15 | } 16 | ] 17 | } 18 | }, 19 | "content-security-policy": "default-src 'self' 'unsafe-eval'" 20 | } 21 | -------------------------------------------------------------------------------- /public/org.image-builder.cockpit-composer.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.image-builder.cockpit-composer 4 | CC0-1.0 5 | Image Builder 6 | 7 | Build customized operating system images 8 | 9 | 10 |

11 | Image Builder (Composer) can generate custom images suitable for deploying 12 | systems, or as images ready to upload to the cloud. 13 |

14 |
15 | org.cockpit_project.cockpit 16 | https://github.com/osbuild/cockpit-composer/ 17 | composer 18 |
19 | -------------------------------------------------------------------------------- /rpmversion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generate the version and release strings to use in the spec file. 4 | # The output of this script is "-", 5 | # where is 1 for the actual release tag, 2 for one commit after the 6 | # release, and so on. 7 | 8 | # Try to git describe. If that fails, just fall back to the version in package.json 9 | gitdesc="$(git describe --exclude '*jenkins*' 2>/dev/null)" 10 | if [ $? -ne 0 ]; then 11 | echo "$(jq -r .version package.json)-1%{?dist}" 12 | else 13 | # Git describe will output either "" for an exact match, 14 | # or "--g" if HEAD is newer than the tag 15 | if ! echo "$gitdesc" | grep -q -- - ;then 16 | echo "${gitdesc}-1%{?dist}" 17 | else 18 | # Add 1 to the number of commits 19 | version="$(echo "$gitdesc" | sed 's/-.*//')" 20 | pkgrel="$(("$(echo "$gitdesc" | sed 's/.*-\([[:digit:]]\+\)-g.*/\1/')" + 1))" 21 | echo "${version}-${pkgrel}%{?dist}" 22 | fi 23 | fi 24 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #main { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { HashRouter, Routes, Route } from "react-router-dom"; 4 | import { IntlProvider } from "react-intl"; 5 | import { Provider } from "react-redux"; 6 | 7 | import store from "./store"; 8 | import { fetchImageTypes, fetchAllImages } from "./slices/imagesSlice"; 9 | import { fetchBlueprints } from "./slices/blueprintsSlice"; 10 | import { getAPIStatus } from "./api"; 11 | 12 | // import patternfly's base style and addons 13 | import "@patternfly/react-core/dist/styles/base.css"; 14 | import "@patternfly/patternfly/patternfly-addons.css"; 15 | // App wide custom styles 16 | import "./App.css"; 17 | 18 | import BlueprintDetails from "./pages/blueprintDetails"; 19 | import BlueprintList from "./pages/blueprintList"; 20 | import APIEmpty from "./components/EmptyStates/APIEmpty"; 21 | 22 | store.dispatch(fetchImageTypes()); 23 | // Fetch blueprints and images every 30 seconds in case of changes via the cli 24 | store.dispatch(fetchBlueprints()); 25 | setInterval(() => store.dispatch(fetchBlueprints()), 1000 * 30); 26 | store.dispatch(fetchAllImages()); 27 | setInterval(() => store.dispatch(fetchAllImages()), 1000 * 30); 28 | 29 | const Content = () => { 30 | const [isAPIOn, setIsAPIOn] = useState(); 31 | 32 | useEffect(() => { 33 | const response = getAPIStatus(); 34 | response.then(() => setIsAPIOn(true)).catch(() => setIsAPIOn(false)); 35 | }, []); 36 | 37 | const Router = () => ( 38 | 39 | 40 | } /> 41 | } /> 42 | 43 | 44 | 45 | ); 46 | 47 | return isAPIOn ? : ; 48 | }; 49 | 50 | const main = async () => { 51 | // cockpit's language is stored in localStorage as `cockpit.lang` 52 | // https://github.com/cockpit-project/cockpit/blob/bafd1067f7f8b9d16479c90bd77dcdda08f044ce/pkg/shell/shell-modals.jsx#L104C38-L104C50 53 | const cockpitLocale = localStorage.getItem("cockpit.lang"); 54 | // browser default language 55 | const userLocale = window.navigator.language; 56 | // use cockpit's language if it's available 57 | // otherwise use the browser's default language 58 | const locale = cockpitLocale || userLocale; 59 | // strip region code 60 | const language = locale.split("-")[0]; 61 | let translations; 62 | try { 63 | translations = (await import(`../translations/compiled/${language}.json`)) 64 | .default; 65 | } catch (error) { 66 | console.error(error); 67 | translations = (await import("../translations/compiled/en.json")).default; 68 | } 69 | 70 | ReactDOM.render( 71 | 72 | 78 | 79 | 80 | , 81 | document.getElementById("main") 82 | ); 83 | }; 84 | 85 | main(); 86 | -------------------------------------------------------------------------------- /src/components/Cards/UserCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Card, 5 | CardTitle, 6 | CardBody, 7 | CardFooter, 8 | ClipboardCopy, 9 | ClipboardCopyVariant, 10 | Title, 11 | DescriptionList, 12 | DescriptionListGroup, 13 | DescriptionListTerm, 14 | DescriptionListDescription, 15 | Divider, 16 | } from "@patternfly/react-core"; 17 | import { CheckCircleIcon, TimesCircleIcon } from "@patternfly/react-icons"; 18 | import { FormattedMessage } from "react-intl"; 19 | 20 | const UserCard = ({ user }) => { 21 | return ( 22 | 23 | 24 | 25 | <FormattedMessage defaultMessage="User" /> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {user?.name} 37 | 38 | 39 | 40 | Admin 41 | 42 | {user?.groups?.includes("wheel") ? ( 43 | 44 | ) : ( 45 | 46 | )} 47 | 48 | 49 | 50 | Password 51 | 52 | {user?.password?.length ? ( 53 | 54 | ) : ( 55 | 56 | )} 57 | 58 | 59 | 60 | SSH key 61 | 62 | {user?.key ? ( 63 | 68 | {user?.key} 69 | 70 | ) : ( 71 | 72 | )} 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | UserCard.propTypes = { 83 | user: PropTypes.object, 84 | }; 85 | 86 | export default UserCard; 87 | -------------------------------------------------------------------------------- /src/components/EmptyStates/APIEmpty.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Title, 4 | EmptyState, 5 | EmptyStateIcon, 6 | EmptyStateBody, 7 | Button, 8 | EmptyStateVariant, 9 | Spinner, 10 | } from "@patternfly/react-core"; 11 | import { CubesIcon } from "@patternfly/react-icons"; 12 | import { FormattedMessage } from "react-intl"; 13 | import cockpit from "cockpit"; 14 | import { getAPIStatus } from "../../api"; 15 | 16 | const APIEmpty = () => { 17 | const [isLoading, setIsLoading] = useState(false); 18 | 19 | const handleClick = () => { 20 | setIsLoading(true); 21 | const argv = ["systemctl", "enable", "--now", "osbuild-composer.socket"]; 22 | cockpit 23 | .spawn(argv, { superuser: "require", err: "message" }) 24 | .then(() => getAPIStatus()) 25 | .then(() => window.location.reload()); 26 | }; 27 | 28 | const Content = () => ( 29 | 30 | 31 | 32 | <FormattedMessage defaultMessage="OSBuild Composer is not started" /> 33 | 34 | 35 | 38 | 39 | ); 40 | 41 | const Loading = () => ( 42 | 43 | 44 | 45 | <FormattedMessage defaultMessage="Starting OSBuild Composer" /> 46 | 47 | 48 | ); 49 | 50 | return isLoading ? : ; 51 | }; 52 | 53 | export default APIEmpty; 54 | -------------------------------------------------------------------------------- /src/components/EmptyStates/BlueprintsEmpty.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Title, 4 | EmptyState, 5 | EmptyStateIcon, 6 | EmptyStateBody, 7 | } from "@patternfly/react-core"; 8 | import { CubesIcon } from "@patternfly/react-icons"; 9 | import { FormattedMessage } from "react-intl"; 10 | import BlueprintWizard from "../Wizard/BlueprintWizard"; 11 | 12 | const BlueprintsEmpty = () => ( 13 | 14 | 15 | 16 | <FormattedMessage defaultMessage="No blueprints" /> 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | export default BlueprintsEmpty; 24 | -------------------------------------------------------------------------------- /src/components/EmptyStates/ImagesEmpty.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Title, 5 | EmptyState, 6 | EmptyStateIcon, 7 | EmptyStateBody, 8 | } from "@patternfly/react-core"; 9 | import { CubesIcon } from "@patternfly/react-icons"; 10 | import { FormattedMessage } from "react-intl"; 11 | import CreateImageWizard from "../Wizard/CreateImageWizard"; 12 | 13 | const ImagesEmpty = ({ blueprint }) => ( 14 | 15 | 16 | 17 | <FormattedMessage defaultMessage="No images" /> 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | ImagesEmpty.propTypes = { 25 | blueprint: PropTypes.object, 26 | }; 27 | 28 | export default ImagesEmpty; 29 | -------------------------------------------------------------------------------- /src/components/EmptyStates/PackagesEmpty.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Title, 5 | EmptyState, 6 | EmptyStateIcon, 7 | EmptyStateBody, 8 | } from "@patternfly/react-core"; 9 | import { CubesIcon } from "@patternfly/react-icons"; 10 | import { FormattedMessage } from "react-intl"; 11 | import BlueprintWizard from "../Wizard/BlueprintWizard"; 12 | 13 | const PackagesEmpty = ({ blueprint }) => ( 14 | 15 | 16 | 17 | <FormattedMessage defaultMessage="No packages selected" /> 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | PackagesEmpty.propTypes = { 25 | blueprint: PropTypes.object, 26 | }; 27 | 28 | export default PackagesEmpty; 29 | -------------------------------------------------------------------------------- /src/components/Modal/DeleteBlueprint.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import { Modal, ModalVariant, Button } from "@patternfly/react-core"; 7 | import { deleteBlueprint } from "../../slices/blueprintsSlice"; 8 | 9 | export const DeleteBlueprint = (props) => { 10 | const dispatch = useDispatch(); 11 | const intl = useIntl(); 12 | 13 | const [isModalOpen, setIsModalOpen] = React.useState(false); 14 | 15 | const handleModalToggle = () => { 16 | setIsModalOpen(!isModalOpen); 17 | }; 18 | 19 | const handleSubmit = () => { 20 | dispatch(deleteBlueprint(props.blueprint.name)); 21 | setIsModalOpen(false); 22 | }; 23 | 24 | return ( 25 | <> 26 | 29 | 39 | 40 | , 41 | , 44 | ]} 45 | > 46 |

Are you sure you want to delete the blueprint

47 |

This action cannot be undone.

48 |
49 | 50 | ); 51 | }; 52 | 53 | DeleteBlueprint.propTypes = { 54 | blueprint: PropTypes.object, 55 | }; 56 | 57 | export default DeleteBlueprint; 58 | -------------------------------------------------------------------------------- /src/components/Modal/DeleteImage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import { Modal, ModalVariant, Button } from "@patternfly/react-core"; 7 | import { deleteImage } from "../../slices/imagesSlice"; 8 | 9 | export const DeleteImage = (props) => { 10 | const dispatch = useDispatch(); 11 | const intl = useIntl(); 12 | 13 | const [isModalOpen, setIsModalOpen] = React.useState(false); 14 | 15 | const handleModalToggle = () => { 16 | setIsModalOpen(!isModalOpen); 17 | }; 18 | 19 | const handleSubmit = () => { 20 | dispatch(deleteImage(props.image.id)); 21 | setIsModalOpen(false); 22 | }; 23 | 24 | return ( 25 | <> 26 | 29 | 39 | 40 | , 41 | , 44 | ]} 45 | > 46 | {" "} 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | DeleteImage.propTypes = { 54 | image: PropTypes.object, 55 | }; 56 | 57 | export default DeleteImage; 58 | -------------------------------------------------------------------------------- /src/components/Modal/ExportBlueprint.css: -------------------------------------------------------------------------------- 1 | .pf-c-code-block { 2 | overflow: auto; 3 | } 4 | 5 | .codeblock-toolbar { 6 | background-color: var(--pf-global--BackgroundColor--200); 7 | } 8 | 9 | .codeblock-toolbar .pf-c-button { 10 | background-color: transparent; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Modal/ExportBlueprint.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useIntl, FormattedMessage } from "react-intl"; 4 | import { 5 | CodeBlock, 6 | CodeBlockCode, 7 | Modal, 8 | ModalVariant, 9 | Button, 10 | Select, 11 | SelectOption, 12 | } from "@patternfly/react-core"; 13 | import { CopyIcon, DownloadIcon } from "@patternfly/react-icons"; 14 | 15 | import { getBlueprintTOML, getBlueprintJSON } from "../../api"; 16 | import "./ExportBlueprint.css"; 17 | 18 | export const ExportBlueprint = (props) => { 19 | const intl = useIntl(); 20 | 21 | const [isModalOpen, setIsModalOpen] = React.useState(false); 22 | const [isSelectOpen, setIsSelectOpen] = React.useState(false); 23 | const [selected, setSelected] = React.useState("TOML"); 24 | const [codeJSON, setCodeJSON] = React.useState(""); 25 | const [codeTOML, setCodeTOML] = React.useState(""); 26 | 27 | useEffect(() => { 28 | getCode(); 29 | }, []); 30 | 31 | const getCode = async () => { 32 | const jsonObject = await getBlueprintJSON(props.blueprint.name); 33 | const json = JSON.stringify(jsonObject, null, 2); 34 | const toml = await getBlueprintTOML(props.blueprint.name).catch(() => { 35 | console.log("Error getting TOML"); 36 | return; 37 | }); 38 | setCodeTOML(toml); 39 | setCodeJSON(json); 40 | }; 41 | 42 | const handleModalToggle = () => { 43 | setIsModalOpen(!isModalOpen); 44 | }; 45 | 46 | const handleSelectToggle = () => { 47 | setIsSelectOpen(!isSelectOpen); 48 | }; 49 | 50 | const onSelect = (e, selection) => { 51 | setIsSelectOpen(false); 52 | setSelected(selection); 53 | }; 54 | 55 | const handleCopy = () => { 56 | const code = selected === "TOML" ? codeTOML : codeJSON; 57 | navigator.clipboard.writeText(code); 58 | }; 59 | 60 | const handleDownload = () => { 61 | const code = selected === "TOML" ? codeTOML : codeJSON; 62 | const filename = `${props.blueprint.name}.${selected.toLowerCase()}`; 63 | 64 | const link = document.createElement("a"); 65 | link.href = "data:text/plain;charset=utf-8," + encodeURIComponent(code); 66 | link.download = filename; 67 | 68 | document.body.appendChild(link); 69 | link.click(); 70 | document.body.removeChild(link); 71 | }; 72 | 73 | return ( 74 | <> 75 | 78 | 87 |
88 | 98 |
99 | 102 | 105 |
106 |
107 | 108 | 109 | {selected === "JSON" ? codeJSON : codeTOML} 110 | 111 | 112 |
113 | 114 | ); 115 | }; 116 | 117 | ExportBlueprint.propTypes = { 118 | blueprint: PropTypes.object, 119 | }; 120 | 121 | export default ExportBlueprint; 122 | -------------------------------------------------------------------------------- /src/components/Modal/GroupsCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import FormRenderer from "@data-driven-forms/react-form-renderer/form-renderer"; 7 | import FormTemplate from "@data-driven-forms/pf4-component-mapper/form-template"; 8 | import componentMapper from "@data-driven-forms/pf4-component-mapper/component-mapper"; 9 | 10 | import { 11 | Modal, 12 | ModalVariant, 13 | Card, 14 | CardBody, 15 | CardHeader, 16 | CardTitle, 17 | Bullseye, 18 | EmptyState, 19 | EmptyStateVariant, 20 | EmptyStateIcon, 21 | Title, 22 | Divider, 23 | } from "@patternfly/react-core"; 24 | import { PlusCircleIcon } from "@patternfly/react-icons"; 25 | 26 | import groupsFields from "../../forms/schemas/groups"; 27 | import TextInputGroupWithChips from "../../forms/components/TextInputGroupWithChips"; 28 | import { blueprintToFormState, formStateToBlueprint } from "../../helpers"; 29 | import { updateBlueprint } from "../../slices/blueprintsSlice"; 30 | import TextFieldCustom from "../../forms/components/TextFieldCustom"; 31 | import { 32 | TableComposable, 33 | Tbody, 34 | Td, 35 | Th, 36 | Thead, 37 | Tr, 38 | } from "@patternfly/react-table"; 39 | 40 | export const GroupsCardModal = ({ blueprint }) => { 41 | const dispatch = useDispatch(); 42 | const intl = useIntl(); 43 | 44 | const [isModalOpen, setIsModalOpen] = useState(false); 45 | 46 | const handleModalToggle = () => { 47 | setIsModalOpen(!isModalOpen); 48 | }; 49 | 50 | const handleSaveBlueprint = (formValues) => { 51 | const blueprintData = formStateToBlueprint(formValues); 52 | dispatch(updateBlueprint(blueprintData)); 53 | setIsModalOpen(false); 54 | }; 55 | 56 | const groups = blueprint?.customizations?.group || []; 57 | 58 | const GroupsCard = () => { 59 | return ( 60 | 61 | 62 | 63 | 64 | <FormattedMessage defaultMessage="Groups" /> 65 | 66 | 67 | 68 | 69 | 70 | {Object.keys(groups).length ? ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | {groups.map((group, index) => ( 84 | 85 | {group.name} 86 | {group.gid} 87 | 88 | ))} 89 | 90 | 91 | ) : ( 92 | 93 | 94 | 95 | 96 | <FormattedMessage defaultMessage="Add groups" /> 97 | 98 | 99 | 100 | )} 101 | 102 | 103 | ); 104 | }; 105 | 106 | return ( 107 | <> 108 | 109 | 117 | ( 120 | 121 | )} 122 | componentMapper={{ 123 | ...componentMapper, 124 | "text-input-group-with-chips": TextInputGroupWithChips, 125 | "text-field-custom": TextFieldCustom, 126 | }} 127 | onSubmit={(formValues) => handleSaveBlueprint(formValues)} 128 | onCancel={handleModalToggle} 129 | initialValues={blueprintToFormState(blueprint)} 130 | /> 131 | 132 | 133 | ); 134 | }; 135 | 136 | GroupsCardModal.propTypes = { 137 | blueprint: PropTypes.object, 138 | }; 139 | 140 | export default GroupsCardModal; 141 | -------------------------------------------------------------------------------- /src/components/Modal/ImportBlueprint.css: -------------------------------------------------------------------------------- 1 | .import-blueprint-file-upload .pf-c-file-upload__file-details { 2 | min-height: 30rem; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Modal/ImportBlueprint.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { useIntl, FormattedMessage } from "react-intl"; 4 | import { 5 | FileUpload, 6 | Modal, 7 | ModalVariant, 8 | Button, 9 | } from "@patternfly/react-core"; 10 | 11 | import { 12 | createBlueprint, 13 | createBlueprintTOML, 14 | } from "../../slices/blueprintsSlice"; 15 | 16 | import "./ImportBlueprint.css"; 17 | 18 | export const ImportBlueprint = () => { 19 | const dispatch = useDispatch(); 20 | const intl = useIntl(); 21 | 22 | const [isModalOpen, setIsModalOpen] = React.useState(false); 23 | const [blueprint, setBlueprint] = React.useState(""); 24 | const [filename, setFilename] = React.useState(""); 25 | const [isLoading, setIsLoading] = React.useState(false); 26 | 27 | const handleModalToggle = () => { 28 | handleClear(); 29 | setIsModalOpen(!isModalOpen); 30 | }; 31 | 32 | const handleFileInputChange = (e, file) => { 33 | setFilename(file.name); 34 | }; 35 | 36 | const handleTextOrDataChange = (value) => { 37 | setBlueprint(value); 38 | }; 39 | 40 | const handleClear = () => { 41 | setFilename(""); 42 | setBlueprint(""); 43 | }; 44 | 45 | const handleFileReadStarted = () => { 46 | setIsLoading(true); 47 | }; 48 | 49 | const handleFileReadFinished = () => { 50 | setIsLoading(false); 51 | }; 52 | 53 | const handleImport = () => { 54 | const filetype = filename.split(".")[1]; 55 | if (filetype === "toml") { 56 | dispatch(createBlueprintTOML(blueprint)); 57 | } else { 58 | const blueprintJSON = JSON.parse(blueprint); 59 | dispatch(createBlueprint(blueprintJSON)); 60 | } 61 | handleModalToggle(false); 62 | }; 63 | 64 | return ( 65 | <> 66 | 69 | 78 | 79 | , 80 | , 83 | ]} 84 | > 85 | 105 | 106 | 107 | ); 108 | }; 109 | 110 | export default ImportBlueprint; 111 | -------------------------------------------------------------------------------- /src/components/Modal/KernelCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import FormRenderer from "@data-driven-forms/react-form-renderer/form-renderer"; 7 | import FormTemplate from "@data-driven-forms/pf4-component-mapper/form-template"; 8 | import componentMapper from "@data-driven-forms/pf4-component-mapper/component-mapper"; 9 | 10 | import { 11 | Modal, 12 | ModalVariant, 13 | Card, 14 | CardBody, 15 | CardHeader, 16 | CardTitle, 17 | Bullseye, 18 | EmptyState, 19 | EmptyStateVariant, 20 | EmptyStateIcon, 21 | Title, 22 | Divider, 23 | DescriptionList, 24 | DescriptionListGroup, 25 | DescriptionListTerm, 26 | DescriptionListDescription, 27 | } from "@patternfly/react-core"; 28 | import { PlusCircleIcon } from "@patternfly/react-icons"; 29 | 30 | import { updateBlueprint } from "../../slices/blueprintsSlice"; 31 | import { blueprintToFormState, formStateToBlueprint } from "../../helpers"; 32 | 33 | import TextInputGroupWithChips from "../../forms/components/TextInputGroupWithChips"; 34 | import TextFieldCustom from "../../forms/components/TextFieldCustom"; 35 | import kernelFields from "../../forms/schemas/kernel"; 36 | 37 | export const KernelCardModal = ({ blueprint }) => { 38 | const dispatch = useDispatch(); 39 | const intl = useIntl(); 40 | 41 | const [isModalOpen, setIsModalOpen] = useState(false); 42 | 43 | const handleModalToggle = () => { 44 | setIsModalOpen(!isModalOpen); 45 | }; 46 | 47 | const handleSaveBlueprint = (formValues) => { 48 | const blueprintData = formStateToBlueprint(formValues); 49 | dispatch(updateBlueprint(blueprintData)); 50 | setIsModalOpen(false); 51 | }; 52 | 53 | const kernel = blueprint?.customizations?.kernel || []; 54 | 55 | const KernelCard = () => { 56 | return ( 57 | 58 | 59 | 60 | 61 | <FormattedMessage defaultMessage="Kernel" /> 62 | 63 | 64 | 65 | 66 | 67 | {kernel?.name?.length || kernel?.append?.length ? ( 68 | 69 | 70 | 71 | 72 | 73 | 74 | {kernel?.name} 75 | 76 | 77 | 78 | 79 | 80 | {kernel?.append} 81 | 82 | 83 | 84 | ) : ( 85 | 86 | 87 | 88 | 89 | <FormattedMessage defaultMessage="Add kernel" /> 90 | 91 | 92 | 93 | )} 94 | 95 | 96 | ); 97 | }; 98 | 99 | return ( 100 | <> 101 | 102 | 110 | ( 113 | 114 | )} 115 | componentMapper={{ 116 | ...componentMapper, 117 | "text-input-group-with-chips": TextInputGroupWithChips, 118 | "text-field-custom": TextFieldCustom, 119 | }} 120 | onSubmit={(formValues) => handleSaveBlueprint(formValues)} 121 | onCancel={handleModalToggle} 122 | initialValues={blueprintToFormState(blueprint)} 123 | /> 124 | 125 | 126 | ); 127 | }; 128 | 129 | KernelCardModal.propTypes = { 130 | blueprint: PropTypes.object, 131 | }; 132 | 133 | export default KernelCardModal; 134 | -------------------------------------------------------------------------------- /src/components/Modal/OpenSCAPCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import FormRenderer from "@data-driven-forms/react-form-renderer/form-renderer"; 7 | import FormTemplate from "@data-driven-forms/pf4-component-mapper/form-template"; 8 | import componentMapper from "@data-driven-forms/pf4-component-mapper/component-mapper"; 9 | 10 | import { 11 | Modal, 12 | ModalVariant, 13 | Card, 14 | CardBody, 15 | CardHeader, 16 | CardTitle, 17 | Bullseye, 18 | EmptyState, 19 | EmptyStateVariant, 20 | EmptyStateIcon, 21 | Title, 22 | Divider, 23 | DescriptionList, 24 | DescriptionListGroup, 25 | DescriptionListTerm, 26 | DescriptionListDescription, 27 | } from "@patternfly/react-core"; 28 | import { PlusCircleIcon } from "@patternfly/react-icons"; 29 | 30 | import { updateBlueprint } from "../../slices/blueprintsSlice"; 31 | import { blueprintToFormState, formStateToBlueprint } from "../../helpers"; 32 | 33 | import TextInputGroupWithChips from "../../forms/components/TextInputGroupWithChips"; 34 | import TextFieldCustom from "../../forms/components/TextFieldCustom"; 35 | import openscapFields from "../../forms/schemas/openscap"; 36 | 37 | export const OpenSCAPCardModal = ({ blueprint }) => { 38 | const dispatch = useDispatch(); 39 | const intl = useIntl(); 40 | 41 | const [isModalOpen, setIsModalOpen] = useState(false); 42 | 43 | const handleModalToggle = () => { 44 | setIsModalOpen(!isModalOpen); 45 | }; 46 | 47 | const handleSaveBlueprint = (formValues) => { 48 | const blueprintData = formStateToBlueprint(formValues); 49 | dispatch(updateBlueprint(blueprintData)); 50 | setIsModalOpen(false); 51 | }; 52 | 53 | const openscap = blueprint?.customizations?.openscap || []; 54 | 55 | const OpenSCAPCard = () => { 56 | return ( 57 | 58 | 59 | 60 | 61 | <FormattedMessage defaultMessage="OpenSCAP" /> 62 | 63 | 64 | 65 | 66 | 67 | {Object.keys(openscap).length ? ( 68 | 69 | 70 | 71 | 72 | 73 | 74 | {openscap?.datastream} 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | {openscap?.profile_id} 83 | 84 | 85 | 86 | ) : ( 87 | 88 | 89 | 90 | 91 | <FormattedMessage defaultMessage="Set OpenSCAP" /> 92 | 93 | 94 | 95 | )} 96 | 97 | 98 | ); 99 | }; 100 | 101 | return ( 102 | <> 103 | 104 | 112 | ( 115 | 116 | )} 117 | componentMapper={{ 118 | ...componentMapper, 119 | "text-input-group-with-chips": TextInputGroupWithChips, 120 | "text-field-custom": TextFieldCustom, 121 | }} 122 | onSubmit={(formValues) => handleSaveBlueprint(formValues)} 123 | onCancel={handleModalToggle} 124 | initialValues={blueprintToFormState(blueprint)} 125 | /> 126 | 127 | 128 | ); 129 | }; 130 | 131 | OpenSCAPCardModal.propTypes = { 132 | blueprint: PropTypes.object, 133 | }; 134 | 135 | export default OpenSCAPCardModal; 136 | -------------------------------------------------------------------------------- /src/components/Modal/SSHKeysCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import FormRenderer from "@data-driven-forms/react-form-renderer/form-renderer"; 7 | import FormTemplate from "@data-driven-forms/pf4-component-mapper/form-template"; 8 | import componentMapper from "@data-driven-forms/pf4-component-mapper/component-mapper"; 9 | 10 | import { 11 | Modal, 12 | ModalVariant, 13 | Card, 14 | CardBody, 15 | CardHeader, 16 | CardTitle, 17 | Bullseye, 18 | EmptyState, 19 | EmptyStateVariant, 20 | EmptyStateIcon, 21 | Title, 22 | Divider, 23 | } from "@patternfly/react-core"; 24 | import { PlusCircleIcon } from "@patternfly/react-icons"; 25 | 26 | import sshKeysFields from "../../forms/schemas/sshkeys"; 27 | import TextInputGroupWithChips from "../../forms/components/TextInputGroupWithChips"; 28 | import { blueprintToFormState, formStateToBlueprint } from "../../helpers"; 29 | import { updateBlueprint } from "../../slices/blueprintsSlice"; 30 | import TextFieldCustom from "../../forms/components/TextFieldCustom"; 31 | import { 32 | TableComposable, 33 | Tbody, 34 | Td, 35 | Th, 36 | Thead, 37 | Tr, 38 | } from "@patternfly/react-table"; 39 | 40 | export const SSHKeysCardModal = ({ blueprint }) => { 41 | const dispatch = useDispatch(); 42 | const intl = useIntl(); 43 | 44 | const [isModalOpen, setIsModalOpen] = useState(false); 45 | 46 | const handleModalToggle = () => { 47 | setIsModalOpen(!isModalOpen); 48 | }; 49 | 50 | const handleSaveBlueprint = (formValues) => { 51 | const blueprintData = formStateToBlueprint(formValues); 52 | dispatch(updateBlueprint(blueprintData)); 53 | setIsModalOpen(false); 54 | }; 55 | 56 | const sshkeys = blueprint?.customizations?.sshkey || []; 57 | 58 | const SSHKeysCard = () => { 59 | return ( 60 | 61 | 62 | 63 | 64 | <FormattedMessage defaultMessage="SSH keys" /> 65 | 66 | 67 | 68 | 69 | 70 | {Object.keys(sshkeys).length ? ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | {sshkeys.map((sshkey, index) => ( 84 | 85 | {sshkey.key} 86 | {sshkey.user} 87 | 88 | ))} 89 | 90 | 91 | ) : ( 92 | 93 | 94 | 95 | 96 | <FormattedMessage defaultMessage="Add ssh keys" /> 97 | 98 | 99 | 100 | )} 101 | 102 | 103 | ); 104 | }; 105 | 106 | return ( 107 | <> 108 | 109 | 117 | ( 120 | 121 | )} 122 | componentMapper={{ 123 | ...componentMapper, 124 | "text-input-group-with-chips": TextInputGroupWithChips, 125 | "text-field-custom": TextFieldCustom, 126 | }} 127 | onSubmit={(formValues) => handleSaveBlueprint(formValues)} 128 | onCancel={handleModalToggle} 129 | initialValues={blueprintToFormState(blueprint)} 130 | /> 131 | 132 | 133 | ); 134 | }; 135 | 136 | SSHKeysCardModal.propTypes = { 137 | blueprint: PropTypes.object, 138 | }; 139 | 140 | export default SSHKeysCardModal; 141 | -------------------------------------------------------------------------------- /src/components/Modal/StopBuild.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import { Modal, ModalVariant, Button } from "@patternfly/react-core"; 7 | import { stopImageBuild } from "../../slices/imagesSlice"; 8 | 9 | export const StopBuild = (props) => { 10 | const dispatch = useDispatch(); 11 | const intl = useIntl(); 12 | 13 | const [isModalOpen, setIsModalOpen] = React.useState(false); 14 | 15 | const handleModalToggle = () => { 16 | setIsModalOpen(!isModalOpen); 17 | }; 18 | 19 | const handleSubmit = () => { 20 | dispatch(stopImageBuild(props.image.id)); 21 | setIsModalOpen(false); 22 | }; 23 | 24 | return ( 25 | <> 26 | 29 | 39 | 40 | , 41 | , 44 | ]} 45 | > 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | StopBuild.propTypes = { 54 | image: PropTypes.object, 55 | }; 56 | 57 | export default StopBuild; 58 | -------------------------------------------------------------------------------- /src/components/Modal/ignitionCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch } from "react-redux"; 4 | import { useIntl, FormattedMessage } from "react-intl"; 5 | 6 | import FormRenderer from "@data-driven-forms/react-form-renderer/form-renderer"; 7 | import FormTemplate from "@data-driven-forms/pf4-component-mapper/form-template"; 8 | import componentMapper from "@data-driven-forms/pf4-component-mapper/component-mapper"; 9 | 10 | import { 11 | Modal, 12 | ModalVariant, 13 | Card, 14 | CardBody, 15 | CardHeader, 16 | CardTitle, 17 | Bullseye, 18 | EmptyState, 19 | EmptyStateVariant, 20 | EmptyStateIcon, 21 | Title, 22 | Divider, 23 | DescriptionList, 24 | DescriptionListGroup, 25 | DescriptionListTerm, 26 | DescriptionListDescription, 27 | } from "@patternfly/react-core"; 28 | import { PlusCircleIcon } from "@patternfly/react-icons"; 29 | 30 | import { updateBlueprint } from "../../slices/blueprintsSlice"; 31 | import { blueprintToFormState, formStateToBlueprint } from "../../helpers"; 32 | 33 | import TextInputGroupWithChips from "../../forms/components/TextInputGroupWithChips"; 34 | import TextFieldCustom from "../../forms/components/TextFieldCustom"; 35 | import ignitionFields from "../../forms/schemas/ignition"; 36 | 37 | export const IgnitionCardModal = ({ blueprint }) => { 38 | const dispatch = useDispatch(); 39 | const intl = useIntl(); 40 | 41 | const [isModalOpen, setIsModalOpen] = useState(false); 42 | 43 | const handleModalToggle = () => { 44 | setIsModalOpen(!isModalOpen); 45 | }; 46 | 47 | const handleSaveBlueprint = (formValues) => { 48 | const blueprintData = formStateToBlueprint(formValues); 49 | dispatch(updateBlueprint(blueprintData)); 50 | setIsModalOpen(false); 51 | }; 52 | 53 | const ignition = blueprint?.customizations?.ignition || []; 54 | 55 | const IgnitionCard = () => { 56 | return ( 57 | 58 | 59 | 60 | 61 | <FormattedMessage defaultMessage="Ignition" /> 62 | 63 | 64 | 65 | 66 | 67 | {ignition?.firstboot?.url ? ( 68 | 69 | 70 | 71 | 72 | 73 | 74 | {ignition.firstboot.url} 75 | 76 | 77 | 78 | ) : ( 79 | 80 | 81 | 82 | 83 | <FormattedMessage defaultMessage="Add ignition" /> 84 | 85 | 86 | 87 | )} 88 | 89 | 90 | ); 91 | }; 92 | 93 | return ( 94 | <> 95 | 96 | 104 | ( 107 | 108 | )} 109 | componentMapper={{ 110 | ...componentMapper, 111 | "text-input-group-with-chips": TextInputGroupWithChips, 112 | "text-field-custom": TextFieldCustom, 113 | }} 114 | onSubmit={(formValues) => handleSaveBlueprint(formValues)} 115 | onCancel={handleModalToggle} 116 | initialValues={blueprintToFormState(blueprint)} 117 | /> 118 | 119 | 120 | ); 121 | }; 122 | 123 | IgnitionCardModal.propTypes = { 124 | blueprint: PropTypes.object, 125 | }; 126 | 127 | export default IgnitionCardModal; 128 | -------------------------------------------------------------------------------- /src/components/Notifications/Notifications.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { FormattedMessage } from "react-intl"; 5 | 6 | import { 7 | Alert, 8 | AlertActionCloseButton, 9 | AlertGroup, 10 | } from "@patternfly/react-core"; 11 | import { selectAllAlerts, removeAlert } from "../../slices/alertsSlice"; 12 | 13 | const Notifications = () => { 14 | const dispatch = useDispatch(); 15 | const getAlerts = () => useSelector(selectAllAlerts); 16 | const alerts = getAlerts(); 17 | 18 | return ( 19 | 20 | {alerts.map((alert) => { 21 | switch (alert.type) { 22 | case "composeQueued": { 23 | return ( 24 | {alert.blueprintName}:, 32 | queue: queue, 33 | }} 34 | /> 35 | } 36 | actionClose={ 37 | dispatch(removeAlert(alert.id))} 39 | /> 40 | } 41 | /> 42 | ); 43 | } 44 | case "composeStarted": { 45 | return ( 46 | {alert.blueprintName}:, 54 | queue: ( 55 | 56 | started 57 | 58 | ), 59 | }} 60 | /> 61 | } 62 | actionClose={ 63 | dispatch(removeAlert(alert.id))} 65 | /> 66 | } 67 | /> 68 | ); 69 | } 70 | case "composeSucceeded": { 71 | return ( 72 | {alert.blueprintName}:, 80 | queue: ( 81 | 82 | complete 83 | 84 | ), 85 | }} 86 | /> 87 | } 88 | actionClose={ 89 | dispatch(removeAlert(alert.id))} 91 | /> 92 | } 93 | /> 94 | ); 95 | } 96 | case "composeFailed": { 97 | return ( 98 | 104 | } 105 | actionClose={ 106 | dispatch(removeAlert(alert.id))} 108 | /> 109 | } 110 | > 111 |

{alert.error}

112 |
113 | ); 114 | } 115 | default: 116 | break; 117 | } 118 | })} 119 |
120 | ); 121 | }; 122 | 123 | Notifications.propTypes = { 124 | alerts: PropTypes.arrayOf(PropTypes.object), 125 | removeAlert: PropTypes.func, 126 | }; 127 | 128 | Notifications.defaultProps = { 129 | alerts: [], 130 | removeAlert() {}, 131 | }; 132 | 133 | export default Notifications; 134 | -------------------------------------------------------------------------------- /src/components/Tab/CustomizationsTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Grid, GridItem, Flex, FlexItem } from "@patternfly/react-core"; 4 | import { ServicesCard } from "../Modal/ServicesCard"; 5 | import FirewallCard from "../Modal/FirewallCard"; 6 | import FilesystemCard from "../Modal/FilesystemCard"; 7 | import SSHKeysCardModal from "../Modal/SSHKeysCard"; 8 | import GroupsCardModal from "../Modal/GroupsCard"; 9 | import KernelCardModal from "../Modal/KernelCard"; 10 | import TimezoneCardModal from "../Modal/TimezoneCard"; 11 | import LocaleCardModal from "../Modal/LocaleCard"; 12 | import FDOCardModal from "../Modal/FDOCard"; 13 | import OpenSCAPCardModal from "../Modal/OpenSCAPCard"; 14 | import UserCard from "../Cards/UserCard"; 15 | import IgnitionCardModal from "../Modal/ignitionCard"; 16 | import OtherCardModal from "../Modal/OtherCard"; 17 | 18 | const CustomizationsTab = ({ blueprint }) => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {blueprint?.customizations?.user?.map((u, i) => ( 30 | 31 | 32 | 33 | ))} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | CustomizationsTab.propTypes = { 83 | blueprint: PropTypes.object, 84 | images: PropTypes.array, 85 | }; 86 | 87 | export default CustomizationsTab; 88 | -------------------------------------------------------------------------------- /src/components/Tab/ImagesTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ImageTable from "../Table/ImageTable"; 5 | import ImagesEmpty from "../EmptyStates/ImagesEmpty"; 6 | 7 | const ImagesTab = ({ images, blueprint }) => { 8 | // destructure and restructure array to not sort the store from the table 9 | const cloneImages = [...images]; 10 | return ( 11 |
12 | {cloneImages.length === 0 && } 13 | {cloneImages.length > 0 && ( 14 | 15 | )} 16 |
17 | ); 18 | }; 19 | 20 | ImagesTab.propTypes = { 21 | images: PropTypes.array, 22 | blueprint: PropTypes.object, 23 | }; 24 | 25 | export default ImagesTab; 26 | -------------------------------------------------------------------------------- /src/components/Tab/PackagesTab.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Pagination } from "@patternfly/react-core"; 4 | 5 | import PackagesTable from "../Table/PackagesTable"; 6 | import PackagesToolbar from "../Toolbar/PackagesToolbar"; 7 | import PackagesEmpty from "../EmptyStates/PackagesEmpty"; 8 | 9 | const PackagesTab = ({ blueprint }) => { 10 | const [toggle, setToggle] = useState("additional"); 11 | const [inputValue, setInputValue] = useState(""); 12 | const [page, setPage] = React.useState(1); 13 | const [perPage, setPerPage] = React.useState(20); 14 | const [isSortAscending, setIsSortAscending] = useState(true); 15 | 16 | const filterPackagesByName = (packages, value) => 17 | packages.filter((pack) => pack.name.includes(value)); 18 | 19 | const sortPackagesByName = (packages, isSortAscending) => 20 | packages.sort((a, b) => { 21 | if (a.name < b.name) { 22 | return isSortAscending ? -1 : 1; 23 | } 24 | if (a.name > b.name) { 25 | return isSortAscending ? 1 : -1; 26 | } 27 | return 0; 28 | }); 29 | 30 | const sortAndFilterPackages = (packages) => { 31 | const filteredPackages = filterPackagesByName(packages, inputValue); 32 | return sortPackagesByName(filteredPackages, isSortAscending); 33 | }; 34 | 35 | const onToggleClick = (_isSelected, event) => 36 | setToggle(event.currentTarget.id); 37 | 38 | const onSetPage = (_event, newPage) => { 39 | setPage(newPage); 40 | }; 41 | 42 | const onPerPageSelect = (_event, newPerPage) => { 43 | setPerPage(newPerPage); 44 | setPage(1); 45 | }; 46 | 47 | const packagesList = 48 | toggle === "additional" ? blueprint.packages : blueprint.dependencies; 49 | 50 | const sortedAndFilteredPackages = sortAndFilterPackages(packagesList); 51 | 52 | const itemsStartInclusive = (page - 1) * perPage; 53 | const itemsEndExclusive = itemsStartInclusive + perPage; 54 | 55 | const paginatedList = sortedAndFilteredPackages?.slice( 56 | itemsStartInclusive, 57 | itemsEndExclusive 58 | ); 59 | 60 | return ( 61 |
62 | {blueprint.packages.length === 0 && ( 63 | 64 | )} 65 | {blueprint.packages.length > 0 && ( 66 | <> 67 | 78 | 83 | 92 | 93 | )} 94 |
95 | ); 96 | }; 97 | 98 | PackagesTab.propTypes = { 99 | blueprint: PropTypes.object, 100 | }; 101 | 102 | export default PackagesTab; 103 | -------------------------------------------------------------------------------- /src/components/Tab/SourcesTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Gallery } from "@patternfly/react-core"; 4 | import { SourceCardModal } from "../Modal/SourceCardModal"; 5 | 6 | const SourcesTab = (props) => { 7 | return ( 8 | 17 | {props.sources?.map((source) => ( 18 | 24 | ))} 25 | 26 | 27 | ); 28 | }; 29 | 30 | SourcesTab.propTypes = { 31 | sources: PropTypes.arrayOf(PropTypes.object), 32 | sourceNames: PropTypes.arrayOf(PropTypes.string), 33 | }; 34 | 35 | export default SourcesTab; 36 | -------------------------------------------------------------------------------- /src/components/Table/BlueprintTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { useIntl } from "react-intl"; 5 | import { 6 | TableComposable, 7 | Th, 8 | Thead, 9 | Tr, 10 | Tbody, 11 | Td, 12 | } from "@patternfly/react-table"; 13 | import CreateImageWizard from "../Wizard/CreateImageWizard"; 14 | import DeleteBlueprint from "../Modal/DeleteBlueprint"; 15 | import ExportBlueprint from "../Modal/ExportBlueprint"; 16 | import { formTimestampLabel } from "../../helpers"; 17 | 18 | const getNumberOfAssociatedImages = (images, blueprint) => { 19 | return images.filter((image) => image.blueprint === blueprint.name).length; 20 | }; 21 | 22 | const getLatestCreationDate = (images, blueprint) => { 23 | const imagesByBlueprint = images.filter( 24 | (image) => image.blueprint === blueprint.name 25 | ); 26 | if (imagesByBlueprint.length > 0) { 27 | const latestTimestamp = Math.max( 28 | ...imagesByBlueprint.map((image) => image.job_created) 29 | ); 30 | return formTimestampLabel(latestTimestamp); 31 | } else { 32 | return "-"; 33 | } 34 | }; 35 | 36 | const BlueprintTable = (props) => { 37 | const intl = useIntl(); 38 | 39 | return ( 40 | 44 | 45 | 46 | Name 47 | Version 48 | Last image created 49 | Images built 50 | Packages 51 | 52 | 53 | 54 | {props.blueprints.map((blueprint) => ( 55 | 56 | 57 | {blueprint.name} 58 | 59 | {blueprint.version} 60 | {getLatestCreationDate(props.images, blueprint)} 61 | {getNumberOfAssociatedImages(props.images, blueprint)} 62 | {blueprint?.packages.length} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | ))} 74 | 75 | 76 | ); 77 | }; 78 | 79 | BlueprintTable.propTypes = { 80 | blueprints: PropTypes.array, 81 | images: PropTypes.array, 82 | }; 83 | 84 | export default BlueprintTable; 85 | -------------------------------------------------------------------------------- /src/components/Table/PackagesTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useIntl } from "react-intl"; 4 | import { 5 | TableComposable, 6 | Thead, 7 | Tr, 8 | Th, 9 | Tbody, 10 | Td, 11 | } from "@patternfly/react-table"; 12 | 13 | const PackagesTable = (props) => { 14 | const intl = useIntl(); 15 | 16 | const columnNames = { 17 | name: intl.formatMessage({ defaultMessage: "Name" }), 18 | version: intl.formatMessage({ defaultMessage: "Version" }), 19 | release: intl.formatMessage({ defaultMessage: "Release" }), 20 | }; 21 | 22 | const onSort = () => props.setIsSortAscending(!props.isSortAscending); 23 | 24 | const sortByName = () => ({ 25 | sortBy: { 26 | index: "Name", 27 | direction: props.isSortAscending ? "asc" : "desc", 28 | defaultDirection: "asc", 29 | }, 30 | onSort, 31 | columnIndex: "Name", 32 | }); 33 | 34 | return ( 35 | 36 | 37 | 38 | {columnNames.name} 39 | {columnNames.version} 40 | {columnNames.release} 41 | 42 | 43 | 44 | {props.packages?.map((pack) => ( 45 | 46 | {pack.name} 47 | {pack.version} 48 | {pack.release} 49 | 50 | ))} 51 | 52 | 53 | ); 54 | }; 55 | 56 | PackagesTable.propTypes = { 57 | packages: PropTypes.array, 58 | isSortAscending: PropTypes.bool, 59 | setIsSortAscending: PropTypes.func, 60 | }; 61 | 62 | export default PackagesTable; 63 | -------------------------------------------------------------------------------- /src/components/Toolbar/BlueprintDetailsToolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { 5 | Breadcrumb, 6 | BreadcrumbItem, 7 | Toolbar, 8 | ToolbarItem, 9 | ToolbarContent, 10 | ToolbarGroup, 11 | } from "@patternfly/react-core"; 12 | 13 | import CreateImageWizard from "../Wizard/CreateImageWizard"; 14 | import BlueprintWizard from "../Wizard/BlueprintWizard"; 15 | 16 | const BlueprintDetailsToolbar = (props) => { 17 | return ( 18 | 19 | 20 | 24 | 25 | 26 | Back to blueprints 27 | 28 | {props.blueprint?.name} 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | BlueprintDetailsToolbar.propTypes = { 48 | blueprint: PropTypes.object, 49 | }; 50 | 51 | export default BlueprintDetailsToolbar; 52 | -------------------------------------------------------------------------------- /src/components/Toolbar/BlueprintToolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useIntl } from "react-intl"; 4 | import { 5 | Button, 6 | Toolbar, 7 | ToolbarItem, 8 | ToolbarContent, 9 | ToolbarGroup, 10 | SearchInput, 11 | } from "@patternfly/react-core"; 12 | import { 13 | SortAlphaDownIcon, 14 | SortAlphaDownAltIcon, 15 | } from "@patternfly/react-icons"; 16 | 17 | const BlueprintToolbar = (props) => { 18 | const intl = useIntl(); 19 | const onInputChange = (newValue) => props.setFilterValue(newValue); 20 | const setSortDirection = () => 21 | props.setIsSortAscending(!props.isSortAscending); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | onInputChange("")} 35 | /> 36 | 37 | 38 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | BlueprintToolbar.propTypes = { 57 | blueprintNames: PropTypes.arrayOf(PropTypes.string), 58 | filterValue: PropTypes.string, 59 | setFilterValue: PropTypes.func, 60 | isSortAscending: PropTypes.bool, 61 | setIsSortAscending: PropTypes.func, 62 | }; 63 | 64 | export default BlueprintToolbar; 65 | -------------------------------------------------------------------------------- /src/components/Toolbar/PackagesToolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useIntl } from "react-intl"; 4 | import { 5 | Toolbar, 6 | ToolbarContent, 7 | ToolbarGroup, 8 | ToolbarItem, 9 | SearchInput, 10 | ToggleGroup, 11 | ToggleGroupItem, 12 | Pagination, 13 | } from "@patternfly/react-core"; 14 | 15 | const PackagesToolbar = (props) => { 16 | const intl = useIntl(); 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | props.onInputChange("")} 30 | /> 31 | 32 | 33 | 34 | 35 | 40 | 48 | 56 | 57 | 58 | 59 | 60 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | PackagesToolbar.propTypes = { 76 | blueprintNames: PropTypes.arrayOf(PropTypes.string), 77 | filterValue: PropTypes.string, 78 | setFilterValue: PropTypes.func, 79 | isSortAscending: PropTypes.bool, 80 | setIsSortAscending: PropTypes.func, 81 | onInputChange: PropTypes.func, 82 | inputValue: PropTypes.string, 83 | onToggleClick: PropTypes.func, 84 | toggle: PropTypes.string, 85 | itemCount: PropTypes.number, 86 | perPage: PropTypes.number, 87 | page: PropTypes.number, 88 | onSetPage: PropTypes.func, 89 | onPerPageSelect: PropTypes.func, 90 | }; 91 | 92 | export default PackagesToolbar; 93 | -------------------------------------------------------------------------------- /src/components/Wizard/CreateImageWizard.css: -------------------------------------------------------------------------------- 1 | .pf-c-popover[data-popper-reference-hidden="true"] { 2 | font-weight: initial; 3 | visibility: initial; 4 | pointer-events: initial; 5 | } 6 | 7 | .upload-checkbox-popover-button { 8 | padding-top: 0; 9 | padding-bottom: 0; 10 | } 11 | 12 | .pf-c-dual-list-selector__menu { 13 | --pf-c-dual-list-selector__menu--MinHeight: 20.5rem; 14 | } 15 | 16 | #user-table table thead tr th { 17 | padding-top: 0; 18 | padding-left: 0; 19 | } 20 | 21 | #user-table table tr td { 22 | padding-left: 0; 23 | } 24 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const ImageTypeLabels = { 2 | alibaba: "Alibaba Cloud (.qcow2)", 3 | ami: "Amazon Web Services (.raw)", 4 | "iot-commit": "IoT Commit (.tar)", 5 | google: "Google Cloud Platform (.vhd)", 6 | "hyper-v": "Hyper-V (.vhd)", 7 | "live-iso": "Installer, suitable for USB and DVD (.iso)", 8 | tar: "Disk Archive (.tar)", 9 | openstack: "OpenStack (.qcow2)", 10 | "partitioned-disk": "Disk Image (.img)", 11 | oci: "Oracle Cloud Infrastructure (.qcow2)", 12 | qcow2: "QEMU Image (.qcow2)", 13 | "rhel-edge-commit": "RHEL for Edge Commit (.tar)", 14 | "rhel-edge-container": "RHEL for Edge Container (.tar)", 15 | "rhel-edge-installer": "RHEL for Edge Installer (.iso)", 16 | "edge-commit": "RHEL for Edge Commit (.tar)", 17 | "edge-container": "RHEL for Edge Container (.tar)", 18 | "edge-installer": "RHEL for Edge Installer (.iso)", 19 | "edge-raw-image": "RHEL for Edge Raw Image (.raw.xz)", 20 | "image-installer": "RHEL Installer (.iso)", 21 | "edge-simplified-installer": "RHEL for Edge Simplified Installer (.iso)", 22 | "edge-ami": "RHEL for Edge AMI (.raw)", 23 | "edge-vsphere": "RHEL for Edge VMware vSphere (.vmdk)", 24 | vhd: "Microsoft Azure (.vhd)", 25 | vmdk: "VMware vSphere (.vmdk)", 26 | ova: "VMware vSphere (.ova)", 27 | gce: "Google Cloud Platform (.tar.gz)", 28 | }; 29 | 30 | export const UNIT_KIB = 1024 ** 1; 31 | export const UNIT_MIB = 1024 ** 2; 32 | export const UNIT_GIB = 1024 ** 3; 33 | -------------------------------------------------------------------------------- /src/forms/components/BlueprintSelect.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { FormGroup, Select, SelectOption } from "@patternfly/react-core"; 4 | import useFormApi from "@data-driven-forms/react-form-renderer/use-form-api"; 5 | import useFieldApi from "@data-driven-forms/react-form-renderer/use-field-api"; 6 | import { defineMessages, useIntl } from "react-intl"; 7 | 8 | const messages = defineMessages({ 9 | blueprintSelect: { 10 | defaultMessage: "Select blueprint", 11 | }, 12 | }); 13 | 14 | const BlueprintSelect = ({ label, isRequired, blueprintNames, ...props }) => { 15 | const intl = useIntl(); 16 | const { change, getState } = useFormApi(); 17 | const formValues = getState()?.values; 18 | const [blueprintName, setBlueprintName] = useState( 19 | formValues?.["blueprintName"] 20 | ); 21 | const [isOpen, setIsOpen] = useState(false); 22 | useFieldApi(props); 23 | 24 | const setOutput = (_, selection) => { 25 | if (blueprintName !== selection) { 26 | setBlueprintName(selection); 27 | setIsOpen(false); 28 | change("blueprintName", selection); 29 | } 30 | }; 31 | 32 | return ( 33 | <> 34 | 35 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | BlueprintSelect.propTypes = { 56 | label: PropTypes.node, 57 | isRequired: PropTypes.bool, 58 | blueprintNames: PropTypes.arrayOf(PropTypes.string), 59 | }; 60 | 61 | export default BlueprintSelect; 62 | -------------------------------------------------------------------------------- /src/forms/components/FileSystemConfigButtons.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { FormattedMessage } from "react-intl"; 4 | import { Button } from "@patternfly/react-core"; 5 | import { FormSpy } from "@data-driven-forms/react-form-renderer"; 6 | import WizardContext from "@data-driven-forms/react-form-renderer/wizard-context"; 7 | 8 | const FileSystemConfigButtons = ({ handleNext, handlePrev, nextStep }) => { 9 | const { formOptions } = useContext(WizardContext); 10 | 11 | return ( 12 | 13 | {({ errors }) => ( 14 | <> 15 | 23 | 26 |
27 | 30 |
31 | 32 | )} 33 |
34 | ); 35 | }; 36 | 37 | FileSystemConfigButtons.propTypes = { 38 | handleNext: PropTypes.func, 39 | handlePrev: PropTypes.func, 40 | nextStep: PropTypes.string, 41 | }; 42 | 43 | export default FileSystemConfigButtons; 44 | -------------------------------------------------------------------------------- /src/forms/components/FileSystemConfigToggle.js: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/RedHatInsights/image-builder-frontend 2 | 3 | import React, { useEffect, useState } from "react"; 4 | 5 | import useFieldApi from "@data-driven-forms/react-form-renderer/use-field-api"; 6 | import useFormApi from "@data-driven-forms/react-form-renderer/use-form-api"; 7 | import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core"; 8 | import { useIntl, defineMessages } from "react-intl"; 9 | 10 | const messages = defineMessages({ 11 | toggleLabel: { 12 | defaultMessage: "Automatic partitioning toggle", 13 | }, 14 | auto: { 15 | defaultMessage: "Use automatic partitioning", 16 | }, 17 | manual: { 18 | defaultMessage: "Manually configure partitions", 19 | }, 20 | }); 21 | 22 | const FileSystemConfigToggle = ({ ...props }) => { 23 | const intl = useIntl(); 24 | const { change, getState } = useFormApi(); 25 | const { input } = useFieldApi(props); 26 | const [selected, setSelected] = useState( 27 | getState()?.values?.["filesystem-toggle"] || "auto" 28 | ); 29 | 30 | useEffect(() => { 31 | change(input.name, selected); 32 | }, [selected]); 33 | 34 | const onClick = (_, evt) => { 35 | setSelected(evt.currentTarget.id); 36 | }; 37 | 38 | return ( 39 | <> 40 | 41 | 47 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default FileSystemConfigToggle; 60 | -------------------------------------------------------------------------------- /src/forms/components/FormGroupCustom.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | FormGroup as Pf4FormGroup, 4 | TextContent, 5 | Text, 6 | } from "@patternfly/react-core"; 7 | import PropTypes from "prop-types"; 8 | 9 | const showError = ( 10 | { error, touched, warning, submitError }, 11 | validateOnMount 12 | ) => { 13 | if ((touched || validateOnMount) && error) { 14 | return { validated: "error" }; 15 | } 16 | 17 | if ((touched || validateOnMount) && submitError) { 18 | return { validated: "error" }; 19 | } 20 | 21 | if ((touched || validateOnMount) && warning) { 22 | return { validated: "warning" }; 23 | } 24 | 25 | return { validated: "default" }; 26 | }; 27 | 28 | const FormGroupCustom = ({ 29 | className, 30 | label, 31 | labelIcon, 32 | isRequired, 33 | helperText, 34 | meta, 35 | validateOnMount, 36 | description, 37 | hideLabel, 38 | children, 39 | id, 40 | FormGroupProps, 41 | }) => ( 42 | 55 | {description && ( 56 | 57 | {description} 58 | 59 | )} 60 | {children} 61 | 62 | ); 63 | 64 | FormGroupCustom.propTypes = { 65 | className: PropTypes.string, 66 | label: PropTypes.node, 67 | labelIcon: PropTypes.node, 68 | isRequired: PropTypes.bool, 69 | helperText: PropTypes.node, 70 | meta: PropTypes.object.isRequired, 71 | description: PropTypes.node, 72 | hideLabel: PropTypes.bool, 73 | validateOnMount: PropTypes.bool, 74 | id: PropTypes.string.isRequired, 75 | children: PropTypes.oneOfType([ 76 | PropTypes.element, 77 | PropTypes.arrayOf(PropTypes.element), 78 | ]).isRequired, 79 | FormGroupProps: PropTypes.object, 80 | }; 81 | 82 | export default FormGroupCustom; 83 | -------------------------------------------------------------------------------- /src/forms/components/ImageOutputSelect.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { FormGroup, Select, SelectOption } from "@patternfly/react-core"; 4 | import useFormApi from "@data-driven-forms/react-form-renderer/use-form-api"; 5 | import useFieldApi from "@data-driven-forms/react-form-renderer/use-field-api"; 6 | import { defineMessages, useIntl } from "react-intl"; 7 | import { ImageTypeLabels } from "../../constants"; 8 | 9 | const messages = defineMessages({ 10 | outputType: { 11 | id: "wizard.imageOutput.selectType", 12 | defaultMessage: "Select output type", 13 | }, 14 | }); 15 | 16 | const ImageOutputSelect = ({ label, isRequired, ...props }) => { 17 | const intl = useIntl(); 18 | const { change, getState } = useFormApi(); 19 | const formValues = getState()?.values; 20 | const [outputType, setOutputType] = useState(formValues?.image?.type); 21 | const [isOpen, setIsOpen] = useState(false); 22 | useFieldApi(props); 23 | 24 | const setOutput = (_, selection) => { 25 | if (outputType !== selection) { 26 | setOutputType(selection); 27 | setIsOpen(false); 28 | // reset all image fields on type change 29 | change("image", {}); 30 | change("image.type", selection); 31 | } 32 | }; 33 | 34 | // only show output types that are declared in our constants 35 | const supportedTypes = props.imageTypes.filter( 36 | (outputType) => ImageTypeLabels[outputType] 37 | ); 38 | 39 | return ( 40 | <> 41 | 42 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | ImageOutputSelect.propTypes = { 64 | label: PropTypes.node, 65 | isRequired: PropTypes.bool, 66 | imageTypes: PropTypes.arrayOf(PropTypes.string), 67 | }; 68 | 69 | export default ImageOutputSelect; 70 | -------------------------------------------------------------------------------- /src/forms/components/MountPoint.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { 4 | Select, 5 | SelectOption, 6 | SelectVariant, 7 | TextInput, 8 | } from "@patternfly/react-core"; 9 | import PropTypes from "prop-types"; 10 | 11 | const MountPoint = ({ ...props }) => { 12 | // check '/' last! 13 | const validPrefixes = [ 14 | "/app", 15 | "/boot", 16 | "/data", 17 | "/home", 18 | "/opt", 19 | "/srv", 20 | "/tmp", 21 | "/usr", 22 | "/usr/local", 23 | "/var", 24 | "/", 25 | ]; 26 | const [isOpen, setIsOpen] = useState(false); 27 | const [prefix, setPrefix] = useState("/"); 28 | const [suffix, setSuffix] = useState(""); 29 | 30 | // split 31 | useEffect(() => { 32 | for (let p of validPrefixes) { 33 | if (props.mountpoint.startsWith(p)) { 34 | setPrefix(p); 35 | setSuffix(props.mountpoint.substring(p.length)); 36 | return; 37 | } 38 | } 39 | }, []); 40 | 41 | useEffect(() => { 42 | let suf = suffix; 43 | let mp = prefix; 44 | if (suf) { 45 | if (mp !== "/" && suf[0] !== "/") { 46 | suf = "/" + suf; 47 | } 48 | 49 | mp += suf; 50 | } 51 | 52 | props.onChange(mp); 53 | }, [prefix, suffix]); 54 | 55 | const onToggle = (isOpen) => { 56 | setIsOpen(isOpen); 57 | }; 58 | 59 | const onSelect = (event, selection) => { 60 | setPrefix(selection); 61 | setIsOpen(false); 62 | }; 63 | 64 | return ( 65 | <> 66 | 78 | {prefix !== "/" && ( 79 | setSuffix(v)} 84 | /> 85 | )} 86 | 87 | ); 88 | }; 89 | 90 | MountPoint.propTypes = { 91 | mountpoint: PropTypes.string.isRequired, 92 | onChange: PropTypes.func.isRequired, 93 | }; 94 | 95 | export default MountPoint; 96 | -------------------------------------------------------------------------------- /src/forms/components/SizeUnit.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { 4 | Select, 5 | SelectOption, 6 | SelectVariant, 7 | TextInput, 8 | } from "@patternfly/react-core"; 9 | import PropTypes from "prop-types"; 10 | import { defineMessages, useIntl } from "react-intl"; 11 | 12 | import { UNIT_GIB, UNIT_KIB, UNIT_MIB } from "../../constants"; 13 | 14 | const messages = defineMessages({ 15 | inputAriaLabel: { 16 | defaultMessage: "Size", 17 | }, 18 | }); 19 | 20 | const SizeUnit = ({ ...props }) => { 21 | const intl = useIntl(); 22 | const [isOpen, setIsOpen] = useState(false); 23 | const [unit, setUnit] = useState(props.unit || UNIT_GIB); 24 | const [size, setSize] = useState(props.size || 1); 25 | 26 | useEffect(() => { 27 | props.onChange(size, unit); 28 | }, [unit, size]); 29 | 30 | const onToggle = (isOpen) => { 31 | setIsOpen(isOpen); 32 | }; 33 | 34 | const onSelect = (event, selection) => { 35 | switch (selection) { 36 | case "KiB": 37 | setUnit(UNIT_KIB); 38 | break; 39 | case "MiB": 40 | setUnit(UNIT_MIB); 41 | break; 42 | case "GiB": 43 | setUnit(UNIT_GIB); 44 | break; 45 | } 46 | 47 | setIsOpen(false); 48 | }; 49 | 50 | return ( 51 | <> 52 | setSize(isNaN(parseInt(v)) ? 0 : parseInt(v))} 57 | aria-label={intl.formatMessage(messages.inputAriaLabel)} 58 | /> 59 | 73 | 74 | ); 75 | }; 76 | 77 | SizeUnit.propTypes = { 78 | size: PropTypes.number.isRequired, 79 | unit: PropTypes.number.isRequired, 80 | onChange: PropTypes.func.isRequired, 81 | }; 82 | 83 | export default SizeUnit; 84 | -------------------------------------------------------------------------------- /src/forms/components/SubmitButtonsCustom.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { useIntl, defineMessages, FormattedMessage } from "react-intl"; 4 | import { Button, Tooltip } from "@patternfly/react-core"; 5 | import { FormSpy } from "@data-driven-forms/react-form-renderer"; 6 | import WizardContext from "@data-driven-forms/react-form-renderer/wizard-context"; 7 | import PropTypes from "prop-types"; 8 | 9 | const messages = defineMessages({ 10 | creatingImage: { 11 | id: "wizard.review.creatingImage", 12 | defaultMessage: "Creating image", 13 | }, 14 | createImageTooltip: { 15 | id: "wizard.review.createImageTooltip", 16 | defaultMessage: "An image can only be created after saving the blueprint", 17 | }, 18 | }); 19 | 20 | const SubmitButtonsCustom = ({ buttonLabels: { cancel, submit, back } }) => { 21 | const intl = useIntl(); 22 | const [isSaving, setIsSaving] = useState(false); 23 | const [hasSaved, setHasSaved] = useState(false); 24 | const { handlePrev, formOptions } = useContext(WizardContext); 25 | const updating = useSelector((state) => state.blueprints.updating); 26 | 27 | return ( 28 | 29 | {() => ( 30 | <> 31 | 54 | 55 | 73 | 74 | 82 |
83 | 91 |
92 | 93 | )} 94 |
95 | ); 96 | }; 97 | 98 | SubmitButtonsCustom.propTypes = { 99 | buttonLabels: PropTypes.shape({ 100 | cancel: PropTypes.node, 101 | submit: PropTypes.node, 102 | back: PropTypes.node, 103 | }), 104 | }; 105 | 106 | export default SubmitButtonsCustom; 107 | -------------------------------------------------------------------------------- /src/forms/components/TextFieldCustom.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextInput } from "@patternfly/react-core"; 3 | import PropTypes from "prop-types"; 4 | 5 | import { useFieldApi } from "@data-driven-forms/react-form-renderer"; 6 | import showError from "@data-driven-forms/pf4-component-mapper/show-error"; 7 | import FormGroupCustom from "./FormGroupCustom"; 8 | 9 | const TextFieldCustom = (props) => { 10 | const { 11 | className, 12 | label, 13 | labelIcon, 14 | hideLabel, 15 | isRequired, 16 | helperText, 17 | meta, 18 | validateOnMount, 19 | description, 20 | input, 21 | isReadOnly, 22 | isDisabled, 23 | id, 24 | FormGroupProps, 25 | ...rest 26 | } = useFieldApi(props); 27 | return ( 28 | 42 | 51 | 52 | ); 53 | }; 54 | 55 | TextFieldCustom.propTypes = { 56 | label: PropTypes.node, 57 | validateOnMount: PropTypes.bool, 58 | isReadOnly: PropTypes.bool, 59 | isRequired: PropTypes.bool, 60 | helperText: PropTypes.node, 61 | description: PropTypes.node, 62 | isDisabled: PropTypes.bool, 63 | id: PropTypes.string, 64 | FormGroupProps: PropTypes.object, 65 | }; 66 | 67 | export default TextFieldCustom; 68 | -------------------------------------------------------------------------------- /src/forms/components/TextInputGroupWithChips.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { defineMessages, useIntl } from "react-intl"; 4 | import { 5 | useFormApi, 6 | useFieldApi, 7 | } from "@data-driven-forms/react-form-renderer"; 8 | import { 9 | TextInputGroup, 10 | TextInputGroupMain, 11 | TextInputGroupUtilities, 12 | Button, 13 | Chip, 14 | ChipGroup, 15 | FormGroup, 16 | } from "@patternfly/react-core"; 17 | import { TimesIcon } from "@patternfly/react-icons"; 18 | 19 | const messages = defineMessages({ 20 | inputAriaLabel: { 21 | defaultMessage: "Enter values", 22 | }, 23 | }); 24 | 25 | const TextInputGroupWithChips = ({ label, ...props }) => { 26 | const intl = useIntl(); 27 | const { change } = useFormApi(); 28 | const { input } = useFieldApi(props); 29 | const [inputValue, setInputValue] = React.useState(""); 30 | const [currentChips, setCurrentChips] = React.useState(input.value || []); 31 | const textInputGroupRef = React.useRef(); 32 | 33 | useEffect(() => { 34 | change(input.name, currentChips); 35 | }, [currentChips]); 36 | 37 | const handleInputChange = (value) => { 38 | setInputValue(value); 39 | }; 40 | 41 | const addChip = (newChipText) => { 42 | setCurrentChips([...currentChips, `${newChipText}`]); 43 | setInputValue(""); 44 | }; 45 | 46 | const deleteChip = (chipToDelete) => { 47 | const newChips = currentChips.filter( 48 | (chip) => !Object.is(chip, chipToDelete) 49 | ); 50 | setCurrentChips(newChips); 51 | }; 52 | 53 | const clearChipsAndInput = () => { 54 | setCurrentChips([]); 55 | setInputValue(""); 56 | }; 57 | 58 | const handleEnter = () => { 59 | if (inputValue.length) { 60 | addChip(inputValue); 61 | focusTextInput(); 62 | } 63 | }; 64 | 65 | const handleTextInputKeyDown = (event) => { 66 | if (event.key === "Enter" || event.key === " " || event.key === ",") { 67 | event.preventDefault(); 68 | handleEnter(); 69 | } 70 | }; 71 | 72 | const focusTextInput = () => { 73 | textInputGroupRef.current.focus(); 74 | }; 75 | 76 | const showClearButton = !!inputValue || !!currentChips.length; 77 | 78 | return ( 79 | 80 | 81 | 89 | 90 | {currentChips.map((currentChip) => ( 91 | deleteChip(currentChip)}> 92 | {currentChip} 93 | 94 | ))} 95 | 96 | 97 | {showClearButton && ( 98 | 99 | 102 | 103 | )} 104 | 105 | 106 | ); 107 | }; 108 | 109 | TextInputGroupWithChips.propTypes = { 110 | label: PropTypes.object, 111 | }; 112 | 113 | export default TextInputGroupWithChips; 114 | -------------------------------------------------------------------------------- /src/forms/components/UploadFile.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useIntl, defineMessages } from "react-intl"; 3 | import PropTypes from "prop-types"; 4 | import { FormGroup, FileUpload } from "@patternfly/react-core"; 5 | import useFormApi from "@data-driven-forms/react-form-renderer/use-form-api"; 6 | import useFieldApi from "@data-driven-forms/react-form-renderer/use-field-api"; 7 | 8 | const messages = defineMessages({ 9 | filenamePlaceholder: { 10 | defaultMessage: "Drag and drop a file or upload one", 11 | }, 12 | }); 13 | 14 | const UploadFile = ({ label, labelIcon, isRequired, ...props }) => { 15 | const intl = useIntl(); 16 | const { change } = useFormApi(); 17 | const { input } = useFieldApi(props); 18 | 19 | const [value, setValue] = useState(""); 20 | const [filename, setFilename] = useState(""); 21 | 22 | useEffect(() => { 23 | change(input.name, value); 24 | }, [value]); 25 | 26 | const handleFileInputChange = (event, file) => { 27 | setFilename(file.name); 28 | }; 29 | 30 | const handleTextOrDataChange = (val) => { 31 | setValue(val); 32 | }; 33 | 34 | const handleClear = () => { 35 | setFilename(""); 36 | setValue(""); 37 | }; 38 | 39 | return ( 40 | <> 41 | 42 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | UploadFile.propTypes = { 59 | label: PropTypes.node, 60 | labelIcon: PropTypes.node, 61 | isRequired: PropTypes.bool, 62 | imageTypes: PropTypes.arrayOf(PropTypes.object), 63 | }; 64 | 65 | export default UploadFile; 66 | -------------------------------------------------------------------------------- /src/forms/components/UploadOCIFile.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useIntl, defineMessages } from "react-intl"; 3 | import PropTypes from "prop-types"; 4 | import { FormGroup, FileUpload } from "@patternfly/react-core"; 5 | import useFormApi from "@data-driven-forms/react-form-renderer/use-form-api"; 6 | import useFieldApi from "@data-driven-forms/react-form-renderer/use-field-api"; 7 | 8 | const messages = defineMessages({ 9 | filenamePlaceholder: { 10 | defaultMessage: "Drag and drop a file or upload one", 11 | }, 12 | }); 13 | 14 | const UploadOCIFile = ({ label, labelIcon, isRequired, ...props }) => { 15 | const intl = useIntl(); 16 | const { change } = useFormApi(); 17 | useFieldApi(props); 18 | 19 | const [filename, setFilename] = useState(""); 20 | 21 | const handleFileInputChange = (event, file) => { 22 | setFilename(file.name); 23 | change("image.upload.settings.filename", file.name); 24 | }; 25 | 26 | const handleTextOrDataChange = (val) => { 27 | change("image.upload.settings.privateKey", val); 28 | }; 29 | 30 | const handleClear = () => { 31 | setFilename(""); 32 | change("image.upload.settings.filename", ""); 33 | change("image.upload.settings.privateKey", ""); 34 | }; 35 | 36 | return ( 37 | <> 38 | 39 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | UploadOCIFile.propTypes = { 55 | label: PropTypes.node, 56 | labelIcon: PropTypes.node, 57 | isRequired: PropTypes.bool, 58 | imageTypes: PropTypes.arrayOf(PropTypes.object), 59 | }; 60 | 61 | export default UploadOCIFile; 62 | -------------------------------------------------------------------------------- /src/forms/schemas/fdo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const fdo = { 5 | fields: [ 6 | { 7 | component: "text-field-custom", 8 | name: "customizations.fdo.manufacturing_server_url", 9 | className: "pf-u-w-75", 10 | type: "text", 11 | label: , 12 | }, 13 | { 14 | component: "text-field-custom", 15 | name: "customizations.fdo.diun_pub_key_insecure", 16 | className: "pf-u-w-75", 17 | label: , 18 | }, 19 | { 20 | component: "text-field-custom", 21 | name: "customizations.fdo.diun_pub_key_hash", 22 | className: "pf-u-w-75", 23 | label: , 24 | }, 25 | { 26 | component: "text-field-custom", 27 | name: "customizations.fdo.diun_pub_key_root_certs", 28 | className: "pf-u-w-75", 29 | label: , 30 | }, 31 | ], 32 | }; 33 | 34 | export default fdo; 35 | -------------------------------------------------------------------------------- /src/forms/schemas/filesystem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import componentTypes from "@data-driven-forms/react-form-renderer/component-types"; 4 | 5 | const messages = defineMessages({ 6 | filesystemToggleLabel: { 7 | defaultMessage: "File system configurations toggle", 8 | }, 9 | filesystemConfigurationLabel: { 10 | defaultMessage: "File system configurations", 11 | }, 12 | }); 13 | 14 | const filesystem = (intl) => { 15 | return { 16 | fields: [ 17 | { 18 | component: componentTypes.PLAIN_TEXT, 19 | name: "filesystem-info", 20 | label: ( 21 | <> 22 | 23 | 24 | 25 | ), 26 | }, 27 | { 28 | component: "filesystem-toggle", 29 | name: "filesystem-toggle", 30 | label: intl.formatMessage(messages.filesystemToggleLabel), 31 | }, 32 | { 33 | component: "filesystem-configuration", 34 | name: "customizations.filesystem", 35 | label: intl.formatMessage(messages.filesystemConfigurationLabel), 36 | validate: [{ type: "filesystemValidator" }], 37 | condition: { 38 | when: "filesystem-toggle", 39 | is: "manual", 40 | }, 41 | }, 42 | ], 43 | }; 44 | }; 45 | 46 | export default filesystem; 47 | -------------------------------------------------------------------------------- /src/forms/schemas/firewall.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const firewall = { 5 | fields: [ 6 | { 7 | component: "text-input-group-with-chips", 8 | name: "customizations.firewall.ports", 9 | label: , 10 | className: "pf-u-w-75", 11 | }, 12 | { 13 | component: "text-input-group-with-chips", 14 | name: "customizations.firewall.services.enabled", 15 | label: , 16 | className: "pf-u-w-75", 17 | }, 18 | { 19 | component: "text-input-group-with-chips", 20 | name: "customizations.firewall.services.disabled", 21 | label: , 22 | className: "pf-u-w-75", 23 | }, 24 | { 25 | component: "field-array", 26 | name: "customizations.firewall.zones", 27 | buttonLabels: { 28 | add: , 29 | remove: , 30 | removeAll: , 31 | }, 32 | fields: [ 33 | { 34 | component: "text-field-custom", 35 | name: "name", 36 | className: "pf-u-w-50", 37 | type: "text", 38 | label: , 39 | }, 40 | { 41 | component: "text-input-group-with-chips", 42 | name: "sources", 43 | label: , 44 | className: "pf-u-w-75", 45 | }, 46 | ], 47 | }, 48 | ], 49 | }; 50 | 51 | export default firewall; 52 | -------------------------------------------------------------------------------- /src/forms/schemas/groups.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | import { dataTypes } from "@data-driven-forms/react-form-renderer"; 4 | 5 | const groups = { 6 | fields: [ 7 | { 8 | component: "field-array", 9 | name: "customizations.group", 10 | buttonLabels: { 11 | add: , 12 | remove: , 13 | removeAll: , 14 | }, 15 | fields: [ 16 | { 17 | component: "text-field-custom", 18 | name: "name", 19 | className: "pf-u-w-50", 20 | type: "text", 21 | label: , 22 | autoFocus: true, 23 | }, 24 | { 25 | component: "text-field-custom", 26 | name: "gid", 27 | className: "pf-u-w-50", 28 | type: "integer", 29 | dataType: dataTypes.INTEGER, 30 | label: , 31 | }, 32 | ], 33 | }, 34 | ], 35 | }; 36 | 37 | export default groups; 38 | -------------------------------------------------------------------------------- /src/forms/schemas/ignition.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const ignition = { 5 | fields: [ 6 | { 7 | component: "text-field-custom", 8 | name: "customizations.ignition.firstboot.url", 9 | className: "pf-u-w-75", 10 | type: "text", 11 | label: , 12 | }, 13 | ], 14 | }; 15 | export default ignition; 16 | -------------------------------------------------------------------------------- /src/forms/schemas/kernel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const kernel = { 5 | fields: [ 6 | { 7 | component: "text-field-custom", 8 | name: "customizations.kernel.name", 9 | className: "pf-u-w-75", 10 | type: "text", 11 | label: , 12 | helperText: , 13 | }, 14 | { 15 | component: "text-field-custom", 16 | name: "customizations.kernel.append", 17 | className: "pf-u-w-75", 18 | type: "text", 19 | label: , 20 | helperText: ( 21 | 22 | ), 23 | }, 24 | ], 25 | }; 26 | 27 | export default kernel; 28 | -------------------------------------------------------------------------------- /src/forms/schemas/locale.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const locale = { 5 | fields: [ 6 | { 7 | component: "text-field-custom", 8 | name: "customizations.locale.keyboard", 9 | className: "pf-u-w-50", 10 | type: "text", 11 | label: , 12 | }, 13 | { 14 | component: "text-input-group-with-chips", 15 | name: "customizations.locale.languages", 16 | label: , 17 | className: "pf-u-w-75", 18 | }, 19 | ], 20 | }; 21 | 22 | export default locale; 23 | -------------------------------------------------------------------------------- /src/forms/schemas/openscap.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const openscap = { 5 | fields: [ 6 | { 7 | component: "text-field-custom", 8 | name: "customizations.openscap.datastream", 9 | className: "pf-u-w-75", 10 | type: "text", 11 | label: , 12 | }, 13 | { 14 | component: "text-field-custom", 15 | name: "customizations.openscap.profile_id", 16 | className: "pf-u-w-75", 17 | label: , 18 | }, 19 | ], 20 | }; 21 | 22 | export default openscap; 23 | -------------------------------------------------------------------------------- /src/forms/schemas/other.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import { Popover, Button } from "@patternfly/react-core"; 4 | import { HelpIcon } from "@patternfly/react-icons"; 5 | 6 | const messages = defineMessages({ 7 | installDevicePopoverBody: { 8 | defaultMessage: "Specify which device the image will be installed onto.", 9 | }, 10 | installDevicePopoverAria: { 11 | defaultMessage: "Installation Device help", 12 | }, 13 | }); 14 | 15 | const other = (intl) => { 16 | return { 17 | fields: [ 18 | { 19 | component: "text-field-custom", 20 | name: "customizations.hostname", 21 | className: "pf-u-w-75", 22 | type: "text", 23 | label: , 24 | helperText: ( 25 | 26 | ), 27 | }, 28 | { 29 | component: "text-field-custom", 30 | name: "customizations.installation_device", 31 | className: "pf-u-w-75", 32 | label: , 33 | labelIcon: ( 34 | 38 | 44 | 45 | ), 46 | helperText: ( 47 | 48 | ), 49 | }, 50 | ], 51 | }; 52 | }; 53 | 54 | export default other; 55 | -------------------------------------------------------------------------------- /src/forms/schemas/services.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const services = { 5 | fields: [ 6 | { 7 | component: "text-input-group-with-chips", 8 | name: "customizations.services.enabled", 9 | label: , 10 | className: "pf-u-w-75", 11 | }, 12 | { 13 | component: "text-input-group-with-chips", 14 | name: "customizations.services.disabled", 15 | label: , 16 | className: "pf-u-w-75", 17 | }, 18 | ], 19 | }; 20 | 21 | export default services; 22 | -------------------------------------------------------------------------------- /src/forms/schemas/sshkeys.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const sshkeys = { 5 | fields: [ 6 | { 7 | component: "field-array", 8 | name: "customizations.sshkey", 9 | buttonLabels: { 10 | add: , 11 | remove: , 12 | removeAll: , 13 | }, 14 | fields: [ 15 | { 16 | component: "textarea", 17 | name: "key", 18 | className: "pf-u-w-50 pf-u-h-25vh", 19 | type: "text", 20 | label: , 21 | autoFocus: true, 22 | validate: [ 23 | { 24 | type: "required", 25 | }, 26 | ], 27 | }, 28 | { 29 | component: "text-field-custom", 30 | name: "user", 31 | className: "pf-u-w-50", 32 | type: "text", 33 | label: , 34 | }, 35 | ], 36 | }, 37 | ], 38 | }; 39 | 40 | export default sshkeys; 41 | -------------------------------------------------------------------------------- /src/forms/schemas/timezone.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const timezone = { 5 | fields: [ 6 | { 7 | component: "text-field-custom", 8 | name: "customizations.timezone.timezone", 9 | className: "pf-u-w-50", 10 | type: "text", 11 | label: , 12 | }, 13 | { 14 | component: "text-input-group-with-chips", 15 | name: "customizations.timezone.ntpservers", 16 | label: , 17 | className: "pf-u-w-75", 18 | }, 19 | ], 20 | }; 21 | 22 | export default timezone; 23 | -------------------------------------------------------------------------------- /src/forms/steps/awsAuth.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React from "react"; 3 | import { defineMessages, FormattedMessage } from "react-intl"; 4 | import validatorTypes from "@data-driven-forms/react-form-renderer/validator-types"; 5 | import { Popover, Button } from "@patternfly/react-core"; 6 | import { HelpIcon } from "@patternfly/react-icons"; 7 | 8 | const messages = defineMessages({ 9 | awsStepsTitle: { 10 | id: "wizard.aws.title", 11 | defaultMessage: "Upload to AWS", 12 | }, 13 | accessKeyPopoverBody: { 14 | id: "wizard.aws.accessKey.popoverBody", 15 | defaultMessage: 16 | "You can create and find existing Access key IDs on the Identity and Access Management (IAM) page in the AWS console.", 17 | }, 18 | accessKeyPopoverAria: { 19 | id: "wizard.aws.accessKey.popoverAria", 20 | defaultMessage: "Access key help", 21 | }, 22 | secretAccessKeyPopoverBody: { 23 | id: "wizard.aws.secretAccessKey.popoverBody", 24 | defaultMessage: 25 | "You can view the Secret access key only when you create a new Access key ID on the " + 26 | "Identity and Access Management (IAM) page in the AWS console.", 27 | }, 28 | secretAccessKeyPopoverAria: { 29 | id: "wizard.aws.secretAccessKey.popoverAria", 30 | defaultMessage: "Secret access key help", 31 | }, 32 | }); 33 | 34 | const awsAuth = (intl) => { 35 | return { 36 | title: , 37 | name: "aws-auth", 38 | substepOf: intl.formatMessage(messages.awsStepsTitle), 39 | nextStep: "aws-dest", 40 | fields: [ 41 | { 42 | component: "text-field-custom", 43 | name: "image.upload.settings.accessKeyID", 44 | className: "pf-u-w-50", 45 | type: "password", 46 | label: , 47 | labelIcon: ( 48 | {str}, 51 | })} 52 | aria-label={intl.formatMessage(messages.accessKeyPopoverAria)} 53 | > 54 | 60 | 61 | ), 62 | isRequired: true, 63 | autoFocus: true, 64 | validate: [ 65 | { 66 | type: validatorTypes.REQUIRED, 67 | }, 68 | ], 69 | }, 70 | { 71 | component: "text-field-custom", 72 | name: "image.upload.settings.secretAccessKey", 73 | className: "pf-u-w-50", 74 | type: "password", 75 | label: , 76 | labelIcon: ( 77 | {str}, 82 | } 83 | )} 84 | aria-label={intl.formatMessage(messages.secretAccessKeyPopoverAria)} 85 | > 86 | 94 | 95 | ), 96 | isRequired: true, 97 | validate: [ 98 | { 99 | type: validatorTypes.REQUIRED, 100 | }, 101 | ], 102 | }, 103 | ], 104 | }; 105 | }; 106 | 107 | export default awsAuth; 108 | -------------------------------------------------------------------------------- /src/forms/steps/azureAuth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import validatorTypes from "@data-driven-forms/react-form-renderer/validator-types"; 4 | import { Popover, Button } from "@patternfly/react-core"; 5 | import { HelpIcon } from "@patternfly/react-icons"; 6 | 7 | const messages = defineMessages({ 8 | azureStepsTitle: { 9 | id: "wizard.azure.title", 10 | defaultMessage: "Upload to Azure", 11 | }, 12 | storageAccountPopoverBody: { 13 | id: "wizard.azure.storageAccount.popoverBody", 14 | defaultMessage: 15 | "Provide the name of a storage account. You can find storage accounts on the " + 16 | "Storage accounts page in the Azure portal.", 17 | }, 18 | storageAccountPopoverAria: { 19 | id: "wizard.azure.storageAccount.popoverAria", 20 | defaultMessage: "Storage account help", 21 | }, 22 | storageAccessKeyPopoverBody: { 23 | id: "wizard.azure.storageAccessKey.popoverBody", 24 | defaultMessage: 25 | "Provide the access key for the desired storage account. You can find the access key on the " + 26 | "Access keys page of the storage account. You can find storage accounts on the " + 27 | "Storage accounts page in the Azure portal.", 28 | }, 29 | storageAccessKeyPopoverAria: { 30 | id: "wizard.azure.storageAccessKey.popoverAria", 31 | defaultMessage: "Storage access key help", 32 | }, 33 | }); 34 | 35 | const azureAuth = (intl) => { 36 | return { 37 | title: ( 38 | 42 | ), 43 | name: "azure-auth", 44 | substepOf: intl.formatMessage(messages.azureStepsTitle), 45 | nextStep: "azure-dest", 46 | fields: [ 47 | { 48 | component: "text-field-custom", 49 | name: "image.upload.settings.storageAccount", 50 | className: "pf-u-w-50", 51 | type: "text", 52 | label: , 53 | labelIcon: ( 54 | {str}, 59 | } 60 | )} 61 | aria-label={intl.formatMessage(messages.storageAccountPopoverAria)} 62 | > 63 | 71 | 72 | ), 73 | isRequired: true, 74 | autoFocus: true, 75 | validate: [ 76 | { 77 | type: validatorTypes.REQUIRED, 78 | }, 79 | ], 80 | }, 81 | { 82 | component: "text-field-custom", 83 | name: "image.upload.settings.storageAccessKey", 84 | className: "pf-u-w-50", 85 | type: "password", 86 | label: , 87 | labelIcon: ( 88 | {str}, 93 | } 94 | )} 95 | aria-label={intl.formatMessage( 96 | messages.storageAccessKeyPopoverAria 97 | )} 98 | > 99 | 107 | 108 | ), 109 | isRequired: true, 110 | validate: [ 111 | { 112 | type: validatorTypes.REQUIRED, 113 | }, 114 | ], 115 | }, 116 | ], 117 | }; 118 | }; 119 | 120 | export default azureAuth; 121 | -------------------------------------------------------------------------------- /src/forms/steps/azureDest.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import validatorTypes from "@data-driven-forms/react-form-renderer/validator-types"; 4 | import { Popover, Button } from "@patternfly/react-core"; 5 | import { HelpIcon } from "@patternfly/react-icons"; 6 | 7 | const messages = defineMessages({ 8 | azureStepsTitle: { 9 | id: "wizard.azure.title", 10 | defaultMessage: "Upload to Azure", 11 | }, 12 | imageNamePopoverBody: { 13 | id: "wizard.azure.imageName.popoverBody", 14 | defaultMessage: 15 | "Provide a file name to be used for the image file that will be uploaded.", 16 | }, 17 | imageNamePopoverAria: { 18 | id: "wizard.azure.imageName.popoverAria", 19 | defaultMessage: "Image name help", 20 | }, 21 | storageContainerPopoverBody: { 22 | id: "wizard.azure.storageContainer.popoverBody", 23 | defaultMessage: 24 | "Provide the Blob container to which the image file will be uploaded. You can find containers under the " + 25 | "Blob service section of a storage account. You can find storage accounts on the " + 26 | "Storage accounts page in the Azure portal.", 27 | }, 28 | storageContainerPopoverAria: { 29 | id: "wizard.azure.storageContainer.popoverAria", 30 | defaultMessage: "Storage container help", 31 | }, 32 | }); 33 | 34 | const azureDest = (intl) => { 35 | return { 36 | title: , 37 | name: "azure-dest", 38 | substepOf: intl.formatMessage(messages.azureStepsTitle), 39 | nextStep: "review-image", 40 | fields: [ 41 | { 42 | component: "text-field-custom", 43 | name: "image.upload.image_name", 44 | className: "pf-u-w-50", 45 | type: "text", 46 | label: , 47 | labelIcon: ( 48 | 52 | 58 | 59 | ), 60 | isRequired: true, 61 | autoFocus: true, 62 | validate: [ 63 | { 64 | type: validatorTypes.REQUIRED, 65 | }, 66 | ], 67 | }, 68 | { 69 | component: "text-field-custom", 70 | name: "image.upload.settings.container", 71 | className: "pf-u-w-50", 72 | type: "text", 73 | label: , 74 | labelIcon: ( 75 | {str}, 80 | } 81 | )} 82 | aria-label={intl.formatMessage( 83 | messages.storageContainerPopoverAria 84 | )} 85 | > 86 | 94 | 95 | ), 96 | isRequired: true, 97 | validate: [ 98 | { 99 | type: validatorTypes.REQUIRED, 100 | }, 101 | ], 102 | }, 103 | ], 104 | }; 105 | }; 106 | 107 | export default azureDest; 108 | -------------------------------------------------------------------------------- /src/forms/steps/blueprintDetails.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import { validatorTypes } from "@data-driven-forms/react-form-renderer"; 4 | 5 | const messages = defineMessages({ 6 | blueprintDetailsStep: { 7 | defaultMessage: "Details", 8 | }, 9 | }); 10 | 11 | const blueprintDetails = (intl) => { 12 | return { 13 | id: "wizard-image-output", 14 | title: intl.formatMessage(messages.blueprintDetailsStep), 15 | name: "blueprint-details", 16 | nextStep: "packages", 17 | fields: [ 18 | { 19 | component: "text-field-custom", 20 | name: "blueprint.name", 21 | className: "pf-u-w-75", 22 | label: , 23 | isRequired: true, 24 | validate: [ 25 | { type: validatorTypes.REQUIRED }, 26 | { type: "blueprintNameValidator" }, 27 | ], 28 | }, 29 | { 30 | component: "text-field-custom", 31 | name: "blueprint.description", 32 | className: "pf-u-w-75", 33 | label: , 34 | }, 35 | ], 36 | }; 37 | }; 38 | 39 | export default blueprintDetails; 40 | -------------------------------------------------------------------------------- /src/forms/steps/fdo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from "react"; 3 | import { defineMessages, FormattedMessage } from "react-intl"; 4 | import fdoFields from "../schemas/fdo"; 5 | 6 | const messages = defineMessages({ 7 | customizationsStepsTitle: { 8 | id: "wizard.customizations.title", 9 | defaultMessage: "Customizations", 10 | }, 11 | }); 12 | 13 | const fdo = (intl) => { 14 | return { 15 | title: , 16 | name: "fdo", 17 | substepOf: intl.formatMessage(messages.customizationsStepsTitle), 18 | nextStep: "openscap", 19 | ...fdoFields, 20 | }; 21 | }; 22 | 23 | export default fdo; 24 | -------------------------------------------------------------------------------- /src/forms/steps/filesystem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import FileSystemConfigButtons from "../components/FileSystemConfigButtons"; 4 | import { FormattedMessage, defineMessages } from "react-intl"; 5 | import filesystemFields from "../schemas/filesystem"; 6 | 7 | const messages = defineMessages({ 8 | customizationsStepTitle: { 9 | defaultMessage: "Customizations", 10 | }, 11 | }); 12 | 13 | const filesystem = (intl) => { 14 | return { 15 | title: , 16 | name: "filesystem", 17 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 18 | buttons: FileSystemConfigButtons, 19 | nextStep: "services", 20 | ...filesystemFields(intl), 21 | }; 22 | }; 23 | 24 | export default filesystem; 25 | -------------------------------------------------------------------------------- /src/forms/steps/firewall.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import firewallFields from "../schemas/firewall"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | buttonsAdd: { 10 | defaultMessage: "Add zone", 11 | }, 12 | buttonsRemove: { 13 | defaultMessage: "Remove zone", 14 | }, 15 | buttonsRemoveAll: { 16 | defaultMessage: "Remove all zones", 17 | }, 18 | }); 19 | 20 | const firewall = (intl) => { 21 | return { 22 | title: , 23 | name: "firewall", 24 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 25 | nextStep: "users", 26 | ...firewallFields, 27 | }; 28 | }; 29 | 30 | export default firewall; 31 | -------------------------------------------------------------------------------- /src/forms/steps/gcp.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import validatorTypes from "@data-driven-forms/react-form-renderer/validator-types"; 4 | import { Popover, Button } from "@patternfly/react-core"; 5 | import { HelpIcon } from "@patternfly/react-icons"; 6 | 7 | const messages = defineMessages({ 8 | imageNamePopoverBody: { 9 | defaultMessage: 10 | "Provide a file name to be used for the image file that will be uploaded.", 11 | }, 12 | imageNamePopoverAria: { 13 | defaultMessage: "Image name help", 14 | }, 15 | bucketPopoverBody: { 16 | defaultMessage: 17 | "Provide the name of the bucket where the image will be uploaded. This bucket must already exist.", 18 | }, 19 | bucketPopoverAria: { 20 | defaultMessage: "Bucket help", 21 | }, 22 | regionPopoverBody: { 23 | defaultMessage: 24 | "Provide the region where the bucket is located. This region can be a regular Google storage region, but also a dual or multi region.", 25 | }, 26 | regionPopoverAria: { 27 | defaultMessage: "Region help", 28 | }, 29 | credentialsPopoverBody: { 30 | defaultMessage: 31 | "The credentials file is a JSON file downloaded from GCP. The credentials are used to determine the GCP project to upload the image to.", 32 | }, 33 | credentialsPopoverAria: { 34 | defaultMessage: "Credentials help", 35 | }, 36 | }); 37 | 38 | const gcp = (intl) => { 39 | return { 40 | title: , 41 | name: "gcp", 42 | nextStep: "review-image", 43 | fields: [ 44 | { 45 | component: "text-field-custom", 46 | name: "image.upload.image_name", 47 | className: "pf-u-w-50", 48 | type: "text", 49 | label: , 50 | labelIcon: ( 51 | 55 | 61 | 62 | ), 63 | isRequired: true, 64 | autoFocus: true, 65 | validate: [ 66 | { 67 | type: validatorTypes.REQUIRED, 68 | }, 69 | ], 70 | }, 71 | { 72 | component: "text-field-custom", 73 | name: "image.upload.settings.region", 74 | className: "pf-u-w-50", 75 | type: "text", 76 | label: , 77 | labelIcon: ( 78 | 82 | 85 | 86 | ), 87 | isRequired: true, 88 | validate: [ 89 | { 90 | type: validatorTypes.REQUIRED, 91 | }, 92 | ], 93 | }, 94 | { 95 | component: "text-field-custom", 96 | name: "image.upload.settings.bucket", 97 | className: "pf-u-w-50", 98 | type: "text", 99 | label: , 100 | labelIcon: ( 101 | 105 | 108 | 109 | ), 110 | isRequired: true, 111 | validate: [ 112 | { 113 | type: validatorTypes.REQUIRED, 114 | }, 115 | ], 116 | }, 117 | { 118 | component: "upload-file", 119 | name: "image.upload.settings.credentials", 120 | className: "pf-u-w-50", 121 | type: "text", 122 | label: , 123 | labelIcon: ( 124 | 128 | 131 | 132 | ), 133 | isRequired: true, 134 | validate: [ 135 | { 136 | type: validatorTypes.REQUIRED, 137 | }, 138 | ], 139 | }, 140 | ], 141 | }; 142 | }; 143 | 144 | export default gcp; 145 | -------------------------------------------------------------------------------- /src/forms/steps/groups.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import groupsFields from "../schemas/groups"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | }); 10 | 11 | const groups = (intl) => { 12 | return { 13 | title: , 14 | name: "groups", 15 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 16 | nextStep: "sshkeys", 17 | ...groupsFields, 18 | }; 19 | }; 20 | 21 | export default groups; 22 | -------------------------------------------------------------------------------- /src/forms/steps/ignition.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from "react"; 3 | import { defineMessages, FormattedMessage } from "react-intl"; 4 | import ignitionFields from "../schemas/ignition"; 5 | 6 | const messages = defineMessages({ 7 | customizationsStepTitle: { 8 | defaultMessage: "Customizations", 9 | }, 10 | }); 11 | 12 | const ignition = (intl) => { 13 | return { 14 | title: , 15 | name: "ignition", 16 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 17 | nextStep: "review-blueprint", 18 | ...ignitionFields, 19 | }; 20 | }; 21 | 22 | export default ignition; 23 | -------------------------------------------------------------------------------- /src/forms/steps/imageOutputStepMapper.js: -------------------------------------------------------------------------------- 1 | export default (props) => { 2 | if (props?.image?.isUpload) { 3 | switch (props?.image?.type) { 4 | case "ami": 5 | case "edge-ami": 6 | return "aws-auth"; 7 | case "oci": 8 | return "oci-auth"; 9 | case "vhd": 10 | return "azure-auth"; 11 | case "vmdk": 12 | case "ova": 13 | case "edge-vsphere": 14 | return "vmware-auth"; 15 | case "gce": 16 | return "gcp"; 17 | default: 18 | return "review-image"; 19 | } 20 | // check if image type is an ostree-settings 21 | } else if ( 22 | [ 23 | "iot-commit", 24 | "edge-commit", 25 | "edge-container", 26 | "edge-installer", 27 | "edge-raw-image", 28 | "edge-simplified-installer", 29 | "edge-ami", 30 | "edge-vsphere", 31 | ].includes(props?.image?.type) 32 | ) { 33 | return "ostree-settings"; 34 | } else { 35 | return "review-image"; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/forms/steps/index.js: -------------------------------------------------------------------------------- 1 | export { default as imageOutput } from "./imageOutput"; 2 | export { default as awsAuth } from "./awsAuth"; 3 | export { default as awsDest } from "./awsDest"; 4 | export { default as azureAuth } from "./azureAuth"; 5 | export { default as azureDest } from "./azureDest"; 6 | export { default as ociAuth } from "./ociAuth"; 7 | export { default as ociDest } from "./ociDest"; 8 | export { default as vmwareAuth } from "./vmwareAuth"; 9 | export { default as vmwareDest } from "./vmwareDest"; 10 | export { default as ostreeSettings } from "./ostreeSettings"; 11 | export { default as packages } from "./packages"; 12 | export { default as users } from "./users"; 13 | export { default as fdo } from "./fdo"; 14 | export { default as blueprintDetails } from "./blueprintDetails"; 15 | export { default as kernel } from "./kernel"; 16 | export { default as filesystem } from "./filesystem"; 17 | export { default as services } from "./services"; 18 | export { default as firewall } from "./firewall"; 19 | export { default as groups } from "./groups"; 20 | export { default as sshkeys } from "./sshkeys"; 21 | export { default as timezone } from "./timezone"; 22 | export { default as locale } from "./locale"; 23 | export { default as other } from "./other"; 24 | export { default as openscap } from "./openscap"; 25 | export { default as ignition } from "./ignition"; 26 | export { default as gcp } from "./gcp"; 27 | export { default as reviewBlueprint } from "./reviewBlueprint"; 28 | export { default as reviewImage } from "./reviewImage"; 29 | -------------------------------------------------------------------------------- /src/forms/steps/kernel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import kernelFields from "../schemas/kernel"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | }); 10 | 11 | const kernel = (intl) => { 12 | return { 13 | title: , 14 | name: "kernel", 15 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 16 | nextStep: "filesystem", 17 | ...kernelFields, 18 | }; 19 | }; 20 | 21 | export default kernel; 22 | -------------------------------------------------------------------------------- /src/forms/steps/locale.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import localeFields from "../schemas/locale"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | }); 10 | 11 | const firewall = (intl) => { 12 | return { 13 | title: , 14 | name: "locale", 15 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 16 | nextStep: "other", 17 | ...localeFields, 18 | }; 19 | }; 20 | 21 | export default firewall; 22 | -------------------------------------------------------------------------------- /src/forms/steps/openscap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from "react"; 3 | import { defineMessages, FormattedMessage } from "react-intl"; 4 | import openscapFields from "../schemas/openscap"; 5 | 6 | const messages = defineMessages({ 7 | customizationsStepTitle: { 8 | defaultMessage: "Customizations", 9 | }, 10 | }); 11 | 12 | const openscap = (intl) => { 13 | return { 14 | title: , 15 | name: "openscap", 16 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 17 | nextStep: "ignition", 18 | ...openscapFields, 19 | }; 20 | }; 21 | 22 | export default openscap; 23 | -------------------------------------------------------------------------------- /src/forms/steps/other.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import otherFields from "../schemas/other"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | installDevicePopoverBody: { 10 | defaultMessage: "Specify which device the image will be installed onto.", 11 | }, 12 | installDevicePopoverAria: { 13 | defaultMessage: "Installation Device help", 14 | }, 15 | }); 16 | 17 | const other = (intl) => { 18 | return { 19 | title: , 20 | name: "other", 21 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 22 | nextStep: "fdo", 23 | ...otherFields(intl), 24 | }; 25 | }; 26 | 27 | export default other; 28 | -------------------------------------------------------------------------------- /src/forms/steps/packages.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | import componentTypes from "@data-driven-forms/react-form-renderer/component-types"; 4 | 5 | const packages = () => { 6 | return { 7 | name: "packages", 8 | title: ( 9 | 10 | ), 11 | nextStep: "kernel", 12 | fields: [ 13 | { 14 | component: componentTypes.PLAIN_TEXT, 15 | name: "packages-text-component", 16 | label: ( 17 | 21 | ), 22 | }, 23 | { 24 | component: "package-selector", 25 | name: "selected-packages", 26 | label: ( 27 | 31 | ), 32 | }, 33 | ], 34 | }; 35 | }; 36 | 37 | export default packages; 38 | -------------------------------------------------------------------------------- /src/forms/steps/reviewBlueprint.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const reviewBlueprint = () => { 5 | return { 6 | name: "review-blueprint", 7 | title: , 8 | fields: [ 9 | { 10 | name: "review", 11 | component: "plain-text", 12 | label: ( 13 | 14 | ), 15 | }, 16 | ], 17 | }; 18 | }; 19 | 20 | export default reviewBlueprint; 21 | -------------------------------------------------------------------------------- /src/forms/steps/reviewImage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormattedMessage } from "react-intl"; 3 | 4 | const reviewImage = () => { 5 | return { 6 | name: "review-image", 7 | title: , 8 | fields: [ 9 | { 10 | name: "review", 11 | component: "plain-text", 12 | label: ( 13 | 14 | ), 15 | }, 16 | ], 17 | }; 18 | }; 19 | 20 | export default reviewImage; 21 | -------------------------------------------------------------------------------- /src/forms/steps/services.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import servicesFields from "../schemas/services"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | }); 10 | 11 | const services = (intl) => { 12 | return { 13 | title: , 14 | name: "services", 15 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 16 | nextStep: "firewall", 17 | ...servicesFields, 18 | }; 19 | }; 20 | 21 | export default services; 22 | -------------------------------------------------------------------------------- /src/forms/steps/sshkeys.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import sshKeysFields from "../schemas/sshkeys"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | buttonsAdd: { 10 | defaultMessage: "Add key", 11 | }, 12 | buttonsRemove: { 13 | defaultMessage: "Remove key", 14 | }, 15 | buttonsRemoveAll: { 16 | defaultMessage: "Remove all keys", 17 | }, 18 | }); 19 | 20 | const groups = (intl) => { 21 | return { 22 | title: , 23 | name: "sshkeys", 24 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 25 | nextStep: "timezone", 26 | ...sshKeysFields, 27 | }; 28 | }; 29 | 30 | export default groups; 31 | -------------------------------------------------------------------------------- /src/forms/steps/timezone.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import timezoneFields from "../schemas/timezone"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | }); 10 | 11 | const firewall = (intl) => { 12 | return { 13 | title: , 14 | name: "timezone", 15 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 16 | nextStep: "locale", 17 | ...timezoneFields, 18 | }; 19 | }; 20 | 21 | export default firewall; 22 | -------------------------------------------------------------------------------- /src/forms/steps/users.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import { validatorTypes } from "@data-driven-forms/react-form-renderer"; 4 | 5 | const messages = defineMessages({ 6 | customizationsStepTitle: { 7 | defaultMessage: "Customizations", 8 | }, 9 | buttonsAdd: { 10 | defaultMessage: "Add user", 11 | }, 12 | buttonsRemove: { 13 | defaultMessage: "Remove user", 14 | }, 15 | buttonsRemoveAll: { 16 | defaultMessage: "Remove all users", 17 | }, 18 | inputUsername: { 19 | defaultMessage: 20 | "Please enter a valid username. Your username can begin with a lower \ 21 | case letter or an underscore, and can only contain lower case letters, \ 22 | digits, underscores, or dashes", 23 | }, 24 | }); 25 | 26 | const users = (intl) => { 27 | return { 28 | title: , 29 | name: "users", 30 | substepOf: intl.formatMessage(messages.customizationsStepTitle), 31 | nextStep: "groups", 32 | fields: [ 33 | { 34 | component: "field-array", 35 | name: "customizations.user", 36 | buttonLabels: { 37 | add: intl.formatMessage(messages.buttonsAdd), 38 | remove: intl.formatMessage(messages.buttonsRemove), 39 | removeAll: intl.formatMessage(messages.buttonsRemoveAll), 40 | }, 41 | fields: [ 42 | { 43 | component: "text-field-custom", 44 | name: "name", 45 | className: "pf-u-w-50", 46 | type: "text", 47 | label: , 48 | isRequired: true, 49 | autoFocus: true, 50 | validate: [ 51 | { 52 | type: "required", 53 | }, 54 | { 55 | type: validatorTypes.PATTERN, 56 | pattern: "^[a-z_][a-z0-9_-]*$", 57 | message: intl.formatMessage(messages.inputUsername), 58 | }, 59 | ], 60 | }, 61 | { 62 | component: "text-field-custom", 63 | name: "password", 64 | className: "pf-u-w-50", 65 | type: "password", 66 | label: , 67 | }, 68 | { 69 | component: "textarea", 70 | name: "key", 71 | className: "pf-u-w-50 pf-u-h-25vh", 72 | type: "text", 73 | label: , 74 | }, 75 | { 76 | component: "checkbox", 77 | name: "isAdmin", 78 | className: "pf-u-w-50", 79 | type: "text", 80 | label: , 81 | }, 82 | ], 83 | }, 84 | ], 85 | }; 86 | }; 87 | 88 | export default users; 89 | -------------------------------------------------------------------------------- /src/forms/steps/vmwareAuth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { defineMessages, FormattedMessage } from "react-intl"; 3 | import validatorTypes from "@data-driven-forms/react-form-renderer/validator-types"; 4 | 5 | const messages = defineMessages({ 6 | vmwareStepsTitle: { 7 | defaultMessage: "Upload to VMware", 8 | }, 9 | }); 10 | 11 | const vmwareAuth = (intl) => { 12 | return { 13 | title: , 14 | name: "vmware-auth", 15 | substepOf: intl.formatMessage(messages.vmwareStepsTitle), 16 | nextStep: "vmware-dest", 17 | fields: [ 18 | { 19 | component: "text-field-custom", 20 | name: "image.upload.settings.username", 21 | className: "pf-u-w-50", 22 | type: "text", 23 | label: , 24 | isRequired: true, 25 | autoFocus: true, 26 | validate: [ 27 | { 28 | type: validatorTypes.REQUIRED, 29 | }, 30 | ], 31 | }, 32 | { 33 | component: "text-field-custom", 34 | name: "image.upload.settings.password", 35 | className: "pf-u-w-50", 36 | type: "password", 37 | label: , 38 | isRequired: true, 39 | validate: [ 40 | { 41 | type: validatorTypes.REQUIRED, 42 | }, 43 | ], 44 | }, 45 | ], 46 | }; 47 | }; 48 | 49 | export default vmwareAuth; 50 | -------------------------------------------------------------------------------- /src/forms/validators/blueprintNameValidator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React from "react"; 3 | import { FormattedMessage } from "react-intl"; 4 | 5 | const blueprintNameValidator = () => (value, allValues) => { 6 | // If we're editing a blueprint, don't validate the name 7 | if (allValues.isEdit) { 8 | return undefined; 9 | } 10 | if (allValues["blueprint-names"].includes(value)) { 11 | return ( 12 | 13 | ); 14 | } 15 | if (value.match(/\s/)) { 16 | return ( 17 | 18 | ); 19 | } 20 | return undefined; 21 | }; 22 | 23 | export default blueprintNameValidator; 24 | -------------------------------------------------------------------------------- /src/forms/validators/filesystemValidator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React from "react"; 3 | import { FormattedMessage } from "react-intl"; 4 | 5 | // contains duplicate mp or no root (/) mp 6 | const filesystemValidator = () => (fsc) => { 7 | if (!fsc) { 8 | return undefined; 9 | } 10 | const mountpoints = fsc.map((fs) => fs.mountpoint); 11 | 12 | if (!mountpoints.length) { 13 | return ( 14 | 15 | ); 16 | } 17 | 18 | const uniqueMountpoints = new Set(mountpoints); 19 | const hasDuplicates = mountpoints.length !== uniqueMountpoints.size; 20 | if (hasDuplicates) { 21 | return ( 22 | 23 | ); 24 | } 25 | 26 | const hasRoot = mountpoints.includes("/"); 27 | if (!hasRoot) { 28 | return ( 29 | 30 | ); 31 | } 32 | }; 33 | 34 | export default filesystemValidator; 35 | -------------------------------------------------------------------------------- /src/forms/validators/hostnameValidator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React from "react"; 3 | import { FormattedMessage } from "react-intl"; 4 | 5 | const hostnameValidator = () => (value) => { 6 | if (!value) { 7 | return undefined; 8 | } 9 | 10 | // https://man7.org/linux/man-pages/man7/hostname.7.html 11 | const regexHostname = /^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}$/; 12 | const validHostname = regexHostname.test(value); 13 | const validSplitLengths = value 14 | .split(".") 15 | .every((element) => element.length > 0 && element.length < 64); 16 | if (!validHostname || !validSplitLengths) { 17 | return ( 18 | 24 | ); 25 | } 26 | }; 27 | 28 | export default hostnameValidator; 29 | -------------------------------------------------------------------------------- /src/forms/validators/index.js: -------------------------------------------------------------------------------- 1 | export { default as ostreeValidator } from "./ostreeValidator"; 2 | export { default as hostnameValidator } from "./hostnameValidator"; 3 | export { default as filesystemValidator } from "./filesystemValidator"; 4 | export { default as blueprintNameValidator } from "./blueprintNameValidator"; 5 | -------------------------------------------------------------------------------- /src/forms/validators/ostreeValidator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React from "react"; 3 | import { FormattedMessage } from "react-intl"; 4 | 5 | const OSTreeValidator = () => (value, formValues) => { 6 | if (!value) { 7 | return undefined; 8 | } 9 | 10 | if ( 11 | formValues["image.ostree.url"]?.length > 0 && 12 | formValues["image.ostree.parent"]?.length > 0 13 | ) { 14 | return ( 15 | 16 | ); 17 | } 18 | }; 19 | 20 | export default OSTreeValidator; 21 | -------------------------------------------------------------------------------- /src/pages/blueprintDetails.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: var(--pf-global--danger-color--100); 3 | } 4 | 5 | .success { 6 | color: var(--pf-global--success-color--100); 7 | } 8 | 9 | .pending { 10 | color: var(--pf-global--info-color--100); 11 | } 12 | 13 | .tabs { 14 | background-color: var(--pf-global--BackgroundColor--100); 15 | } 16 | 17 | .details-tab { 18 | background-color: var(--pf-global--BackgroundColor--100); 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/blueprintDetails.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { useParams } from "react-router-dom"; 4 | import { FormattedMessage } from "react-intl"; 5 | import { 6 | Page, 7 | PageSection, 8 | PageSectionVariants, 9 | Tabs, 10 | Tab, 11 | TabTitleText, 12 | Text, 13 | TextContent, 14 | } from "@patternfly/react-core"; 15 | 16 | import CustomizationsTab from "../components/Tab/CustomizationsTab"; 17 | import PackagesTab from "../components/Tab/PackagesTab"; 18 | import ImagesTab from "../components/Tab/ImagesTab"; 19 | import BlueprintDetailsToolbar from "../components/Toolbar/BlueprintDetailsToolbar"; 20 | import Notifications from "../components/Notifications/Notifications"; 21 | 22 | import { 23 | selectBlueprintByName, 24 | depsolveBlueprint, 25 | } from "../slices/blueprintsSlice"; 26 | import { selectImagesFilteredAndSorted } from "../slices/imagesSlice"; 27 | 28 | import "./blueprintDetails.css"; 29 | 30 | const BlueprintDetails = () => { 31 | const dispatch = useDispatch(); 32 | const getBlueprintByName = (name) => 33 | useSelector((state) => selectBlueprintByName(state, name)); 34 | const blueprintName = useParams().blueprint; 35 | 36 | useEffect(() => { 37 | dispatch(depsolveBlueprint(blueprintName)); 38 | }, []); 39 | 40 | const blueprint = getBlueprintByName(blueprintName); 41 | // images sorted by creation date on default 42 | const images = useSelector((state) => 43 | selectImagesFilteredAndSorted(state, { 44 | key: "blueprint", 45 | value: blueprintName, 46 | sortBy: "job_created", 47 | isSortAscending: false, 48 | }) 49 | ); 50 | 51 | const [activeTab, setActiveTab] = useState("customizations"); 52 | 53 | const handleTabClick = (event, tabName) => setActiveTab(tabName); 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {blueprint?.name} 64 | {blueprint?.description} 65 | 66 | 67 | 68 | 74 | 78 | 79 | 80 | } 81 | > 82 | 87 | 88 | 92 | 93 | 94 | } 95 | > 96 | {blueprint?.dependencies?.length && ( 97 | 98 | )} 99 | 100 | 104 | 105 | 106 | } 107 | > 108 | 109 | 110 | 111 | 112 | 113 | ); 114 | }; 115 | 116 | export default BlueprintDetails; 117 | -------------------------------------------------------------------------------- /src/slices/alertsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createEntityAdapter } from "@reduxjs/toolkit"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | export const alertsAdapter = createEntityAdapter({}); 5 | 6 | const initialState = alertsAdapter.getInitialState({}); 7 | 8 | const alertsSlice = createSlice({ 9 | name: "alerts", 10 | initialState, 11 | reducers: { 12 | addAlert: alertsAdapter.addOne, 13 | removeAlert: alertsAdapter.removeOne, 14 | }, 15 | extraReducers: (builder) => { 16 | builder.addCase("images/create/fulfilled", (state, action) => { 17 | alertsAdapter.addOne(state, { 18 | id: uuidv4(), 19 | type: "composeQueued", 20 | blueprintName: action.payload, 21 | }); 22 | }); 23 | builder.addCase("images/create/rejected", (state, action) => { 24 | alertsAdapter.addOne(state, { 25 | id: uuidv4(), 26 | type: "composeFailed", 27 | error: action.payload, 28 | }); 29 | }); 30 | }, 31 | }); 32 | 33 | export const { removeAlert } = alertsSlice.actions; 34 | export default alertsSlice.reducer; 35 | 36 | // Can create a set of memoized selectors based on the location of this entity state 37 | // Export the customized selectors for this adapter using `getSelectors` 38 | export const { selectAll: selectAllAlerts, selectById: selectAlertByName } = 39 | alertsAdapter.getSelectors((state) => state.alerts); 40 | -------------------------------------------------------------------------------- /src/slices/blueprintsSlice.js: -------------------------------------------------------------------------------- 1 | import { 2 | createSlice, 3 | createAsyncThunk, 4 | createEntityAdapter, 5 | createSelector, 6 | } from "@reduxjs/toolkit"; 7 | import * as api from "../api"; 8 | 9 | export const blueprintsAdapter = createEntityAdapter({ 10 | // the id for each blueprint is the blueprint name 11 | selectId: (blueprint) => blueprint.name, 12 | }); 13 | 14 | const initialState = blueprintsAdapter.getInitialState({}); 15 | 16 | export const createBlueprint = createAsyncThunk( 17 | "blueprints/create", 18 | async (blueprint) => { 19 | await api.createBlueprint(blueprint); 20 | return blueprint; 21 | } 22 | ); 23 | 24 | export const createBlueprintTOML = createAsyncThunk( 25 | "blueprints/createTOML", 26 | async (blueprint, { dispatch }) => { 27 | await api.createBlueprintTOML(blueprint); 28 | dispatch(fetchBlueprints()); 29 | } 30 | ); 31 | 32 | export const updateBlueprint = createAsyncThunk( 33 | "blueprints/update", 34 | async (blueprint) => { 35 | await api.createBlueprint(blueprint); 36 | return blueprint; 37 | } 38 | ); 39 | 40 | export const fetchBlueprints = createAsyncThunk( 41 | "blueprints/fetchAll", 42 | async () => { 43 | const names = await api.getBlueprintsNames(); 44 | const blueprints = await api.getBlueprintsInfo(names); 45 | return blueprints; 46 | } 47 | ); 48 | 49 | export const depsolveBlueprint = createAsyncThunk( 50 | "blueprints/depsolve", 51 | async (blueprintName) => { 52 | const response = await api.depsolveBlueprint(blueprintName); 53 | const blueprint = response.blueprints[0].blueprint; 54 | const dependencies = response.blueprints[0].dependencies; 55 | const packages = blueprint.packages.map((pkg) => 56 | response.blueprints[0].dependencies.find((dep) => dep.name === pkg.name) 57 | ); 58 | blueprint.packages = packages; 59 | blueprint.dependencies = dependencies; 60 | return blueprint; 61 | } 62 | ); 63 | 64 | export const deleteBlueprint = createAsyncThunk( 65 | "blueprints/delete", 66 | async (blueprintName) => { 67 | await api.deleteBlueprint(blueprintName); 68 | return blueprintName; 69 | } 70 | ); 71 | 72 | const blueprintsSlice = createSlice({ 73 | name: "blueprints", 74 | initialState, 75 | reducers: {}, 76 | extraReducers: (builder) => { 77 | builder.addCase(createBlueprint.fulfilled, blueprintsAdapter.addOne); 78 | builder.addCase(fetchBlueprints.pending, (state) => { 79 | state.fetching = true; 80 | }); 81 | builder.addCase(fetchBlueprints.fulfilled, (state, action) => { 82 | state.fetching = false; 83 | blueprintsAdapter.upsertMany(state, action.payload); 84 | }); 85 | builder.addCase(updateBlueprint.fulfilled, blueprintsAdapter.upsertOne); 86 | builder.addCase(depsolveBlueprint.fulfilled, blueprintsAdapter.upsertOne); 87 | builder.addCase(deleteBlueprint.fulfilled, blueprintsAdapter.removeOne); 88 | }, 89 | }); 90 | 91 | export default blueprintsSlice.reducer; 92 | 93 | // Can create a set of memoized selectors based on the location of this entity state 94 | // Export the customized selectors for this adapter using `getSelectors` 95 | export const { 96 | selectAll: selectAllBlueprints, 97 | selectById: selectBlueprintByName, 98 | selectIds: selectAllBlueprintNames, 99 | } = blueprintsAdapter.getSelectors((state) => state.blueprints); 100 | 101 | const filterBlueprints = (blueprints, { key, value }) => 102 | blueprints.filter((blueprint) => blueprint[key].includes(value)); 103 | 104 | const sortBlueprints = (blueprints, { sortBy, isSortAscending }) => 105 | blueprints.sort((a, b) => { 106 | if (a[sortBy] < b[sortBy]) { 107 | return isSortAscending ? -1 : 1; 108 | } 109 | if (a[sortBy] > b[sortBy]) { 110 | return isSortAscending ? 1 : -1; 111 | } 112 | return 0; 113 | }); 114 | 115 | export const selectBlueprintsFiltered = createSelector( 116 | selectAllBlueprints, 117 | (_, args) => args, 118 | filterBlueprints 119 | ); 120 | 121 | export const selectBlueprintsFilteredAndSorted = createSelector( 122 | selectBlueprintsFiltered, 123 | (_, args) => args, 124 | sortBlueprints 125 | ); 126 | -------------------------------------------------------------------------------- /src/slices/imagesSlice.js: -------------------------------------------------------------------------------- 1 | import { 2 | createSlice, 3 | createAsyncThunk, 4 | createEntityAdapter, 5 | createSelector, 6 | } from "@reduxjs/toolkit"; 7 | import * as api from "../api"; 8 | 9 | export const imagesAdapter = createEntityAdapter({}); 10 | 11 | const initialState = imagesAdapter.getInitialState({ 12 | types: [], 13 | }); 14 | 15 | export const fetchImageTypes = createAsyncThunk( 16 | "images/fetchTypes", 17 | async () => { 18 | const imageTypes = await api.getImageTypes(); 19 | return imageTypes.map((imageType) => imageType.name); 20 | } 21 | ); 22 | 23 | export const fetchImage = createAsyncThunk("images/fetch", async (imageId) => { 24 | return await api.getImageStatus(imageId); 25 | }); 26 | 27 | export const fetchAllImages = createAsyncThunk("images/fetchAll", async () => { 28 | return await api.getAllImageStatus(); 29 | }); 30 | 31 | export const createImage = createAsyncThunk( 32 | "images/create", 33 | async (args, { dispatch, rejectWithValue }) => { 34 | const { blueprintName, type, size, ostree, upload } = args; 35 | const sizeBytes = size * 1024 * 1024 * 1024; 36 | try { 37 | const response = await api.createImage( 38 | blueprintName, 39 | type, 40 | sizeBytes, 41 | ostree, 42 | upload 43 | ); 44 | const imageId = response.build_id; 45 | dispatch(fetchImage(imageId)); 46 | return blueprintName; 47 | } catch (error) { 48 | const msg = error?.body?.errors[0]?.msg; 49 | return rejectWithValue(msg); 50 | } 51 | } 52 | ); 53 | 54 | export const deleteImage = createAsyncThunk( 55 | "images/delete", 56 | async (imageId) => { 57 | await api.deleteImage(imageId); 58 | return imageId; 59 | } 60 | ); 61 | 62 | export const stopImageBuild = createAsyncThunk( 63 | "images/stopBuild", 64 | async (imageId, { dispatch }) => { 65 | await api.cancelImage(imageId); 66 | dispatch(fetchImage(imageId)); 67 | return; 68 | } 69 | ); 70 | 71 | const imagesSlice = createSlice({ 72 | name: "images", 73 | initialState, 74 | reducers: {}, 75 | extraReducers: (builder) => { 76 | builder.addCase(fetchImageTypes.fulfilled, (state, action) => { 77 | state.types = action.payload; 78 | }); 79 | builder.addCase(fetchAllImages.fulfilled, imagesAdapter.upsertMany); 80 | builder.addCase(deleteImage.fulfilled, imagesAdapter.removeOne); 81 | builder.addCase(fetchImage.fulfilled, imagesAdapter.upsertOne); 82 | }, 83 | }); 84 | 85 | export default imagesSlice.reducer; 86 | 87 | export const selectAllImageTypes = (state) => state.images.types; 88 | 89 | // Can create a set of memoized selectors based on the location of this entity state 90 | // Export the customized selectors for this adapter using `getSelectors` 91 | export const { 92 | selectAll: selectAllImages, 93 | selectById: selectImageById, 94 | selectIds: selectAllImageIds, 95 | } = imagesAdapter.getSelectors((state) => state.images); 96 | 97 | const filterImages = (images, { key, value }) => 98 | images.filter((image) => image[key] === value); 99 | 100 | const sortImages = (images, { sortBy, isSortAscending }) => 101 | images.sort((a, b) => { 102 | if (a[sortBy] < b[sortBy]) { 103 | return isSortAscending ? -1 : 1; 104 | } 105 | if (a[sortBy] > b[sortBy]) { 106 | return isSortAscending ? 1 : -1; 107 | } 108 | return 0; 109 | }); 110 | 111 | export const selectImagesFiltered = createSelector( 112 | selectAllImages, 113 | (_, args) => args, 114 | filterImages 115 | ); 116 | 117 | export const selectImagesFilteredAndSorted = createSelector( 118 | selectImagesFiltered, 119 | (_, args) => args, 120 | sortImages 121 | ); 122 | -------------------------------------------------------------------------------- /src/slices/sourcesSlice.js: -------------------------------------------------------------------------------- 1 | import { 2 | createSlice, 3 | createAsyncThunk, 4 | createEntityAdapter, 5 | } from "@reduxjs/toolkit"; 6 | import * as api from "../api"; 7 | 8 | export const sourcesAdapter = createEntityAdapter({ 9 | // the id for each source is the source name 10 | selectId: (source) => source.name, 11 | }); 12 | 13 | const initialState = sourcesAdapter.getInitialState({}); 14 | 15 | export const fetchSources = createAsyncThunk("sources/fetchAll", async () => { 16 | return await api.getAllSources(); 17 | }); 18 | 19 | export const createSource = createAsyncThunk( 20 | "sources/create", 21 | async (source) => { 22 | await api.createSource(source); 23 | return source; 24 | } 25 | ); 26 | 27 | export const deleteSource = createAsyncThunk( 28 | "sources/delete", 29 | async (sourceName) => { 30 | await api.deleteSource(sourceName); 31 | return sourceName; 32 | } 33 | ); 34 | 35 | const sourcesSlice = createSlice({ 36 | name: "sources", 37 | initialState, 38 | reducers: {}, 39 | extraReducers: (builder) => { 40 | builder.addCase(fetchSources.fulfilled, sourcesAdapter.upsertMany); 41 | builder.addCase(createSource.fulfilled, sourcesAdapter.upsertOne); 42 | builder.addCase(deleteSource.fulfilled, sourcesAdapter.removeOne); 43 | }, 44 | }); 45 | 46 | export default sourcesSlice.reducer; 47 | 48 | // Can create a set of memoized selectors based on the location of this entity state 49 | // Export the customized selectors for this adapter using `getSelectors` 50 | export const { 51 | selectAll: selectAllSources, 52 | selectIds: selectAllSourceNames, 53 | selectById: selectSourceByName, 54 | } = sourcesAdapter.getSelectors((state) => state.sources); 55 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import alertsReducer from "./slices/alertsSlice"; 3 | import blueprintsReducer from "./slices/blueprintsSlice"; 4 | import imagesReducer from "./slices/imagesSlice"; 5 | import sourcesReducer from "./slices/sourcesSlice"; 6 | 7 | export default configureStore({ 8 | reducer: { 9 | alerts: alertsReducer, 10 | blueprints: blueprintsReducer, 11 | images: imagesReducer, 12 | sources: sourcesReducer, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Cockpit-Composer Integration Test 2 | 3 | The integration test for Cockpit Composer! It is performed on the 4 | application level and tests whether the business requirements are met 5 | regardless of app internal architecture, dependencies, data integrity and such. 6 | Actually we need to follow the end-user flows and assert they get the intended 7 | experience and focus on the behavior of the thing as the user would see it. 8 | 9 | The integration tests are powered by [Cockpit test 10 | framework](https://github.com/cockpit-project/cockpit/tree/main/test), which is simple and easy to 11 | debug for test development. 12 | 13 | ## Requirement 14 | 15 | For testing, the following dependencies are required: 16 | 17 | $ sudo dnf install curl expect xz rpm-build chromium-headless \ 18 | libvirt-daemon-kvm libvirt-client python3-libvirt 19 | 20 | And `chrome-remote-interface` and `sizzle` Javascript libraries need to be installed: 21 | 22 | $ npm install 23 | 24 | ## Introduction 25 | 26 | Before running the tests, ensure Cockpit-Composer environment has been built: 27 | 28 | $ make vm 29 | 30 | To run all tests run the following: 31 | 32 | $ make check 33 | 34 | Alternatively, you can run individual tests. To list the individual test names you can run: 35 | 36 | $ test/common/run-tests --test-dir=test/verify -l 37 | 38 | And then run: 39 | 40 | $ test/common/run-tests --test-dir=test/verify $TEST_NAME 41 | 42 | To run the tests with network, use the `--enable-network` flag: 43 | 44 | $ test/common/run-tests --test-dir=test/verify --enable-network $TEST_NAME 45 | 46 | To see more verbose output from the test, use the `--verbose` and/or `--trace` flags: 47 | 48 | $ test/common/run-tests --test-dir=test/verify --verbose --trace $TEST_NAME 49 | 50 | In addition if you specify `--sit`, then the test will wait on failure and allow you to log into 51 | cockpit and/or the test instance and diagnose the issue. An address will be printed of the test 52 | instance. 53 | 54 | $ test/common/run-tests --test-dir=test/verify --sit $TEST_NAME 55 | 56 | Normally each test starts its own chromium headless browser process on a separate random port. To 57 | interactively follow what a test is doing, set environment variable `$TEST_SHOW_BROWSER`. 58 | 59 | $ TEST_SHOW_BROWSER=true test/common/run-tests --test-dir=test/verify $TEST_NAME 60 | 61 | ## Test Configuration 62 | 63 | You can set these environment variables to configure the test suite: 64 | 65 | TEST_OS The OS to run the tests in. Currently supported values: 66 | "fedora-34, rhel-8-6, rhel-9-0" 67 | "fedora-34" is the default 68 | 69 | TEST_DATA Where to find and store test machine images. The 70 | default is the same directory that this README file is in. 71 | 72 | TEST_CDP_PORT Attach to an actually running browser that is compatible with 73 | the Chrome Debug Protocol, on the given port. Don't use this 74 | with parallel tests. 75 | 76 | TEST_BROWSER What browser should be used for testing. Currently supported values: 77 | "chromium" 78 | "firefox" 79 | "chromium" is the default. 80 | 81 | TEST_SHOW_BROWSER Set to run browser interactively. When not specified, 82 | browser is run in headless mode. 83 | 84 | ## Guidelines for writing tests 85 | 86 | Tests decorated with `@nondestructive` will all run against the same test 87 | machine. The nondestructive test should clean up after itself and restore the 88 | state of the machine, such that the next nondestructive test is not impacted. 89 | 90 | A fast running test suite is more important than independent, 91 | small test cases. 92 | 93 | ## Code coverage from end-to-end tests 94 | 95 | Before running the tests the application code must be instrumented with 96 | istanbul! Then inside the browser scope all coverage information is available 97 | from `window.__coverage__` which needs to be passed back to the node scope 98 | and made available for the reporting tools to use. 99 | 100 | Please include helper method `check_coverage()` at the end of each test. 101 | This helper method will collect coverage result and save result into file 102 | `.nyc_output/coverage-.json`. The hash value is the sha256 sum of 103 | the coverage report itself. Some cases may have identical coverage so 104 | the number of json files will be equal or less to the number of test cases. 105 | 106 | ## Code Style 107 | 108 | Python code in this project should follow 109 | [Flake8](https://www.flake8rules.com/). 110 | -------------------------------------------------------------------------------- /test/browser/browser.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd "${0%/*}/../.." 4 | 5 | # allow test to set up things on the machine 6 | mkdir -p /root/.ssh 7 | curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys 8 | chmod 600 /root/.ssh/authorized_keys 9 | 10 | # create user account for logging in 11 | if ! id admin 2>/dev/null; then 12 | useradd -c Administrator -G wheel admin 13 | echo admin:foobar | chpasswd 14 | fi 15 | 16 | # set root's password 17 | echo root:foobar | chpasswd 18 | 19 | # avoid sudo lecture during tests 20 | su -c 'echo foobar | sudo --stdin whoami' - admin 21 | 22 | # disable core dumps, we rather investigate them upstream where test VMs are accessible 23 | echo core > /proc/sys/kernel/core_pattern 24 | 25 | sh test/vm.install 26 | 27 | # Run tests in the cockpit tasks container, as unprivileged user 28 | CONTAINER="$(cat .cockpit-ci/container)" 29 | if grep -q platform:el10 /etc/os-release; then 30 | # HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2273078 31 | export NETAVARK_FW=nftables 32 | fi 33 | exec podman \ 34 | run \ 35 | --rm \ 36 | --shm-size=1024m \ 37 | --security-opt=label=disable \ 38 | --env='TEST_*' \ 39 | --volume="${TMT_TEST_DATA}":/logs:rw,U --env=LOGS=/logs \ 40 | --volume="$(pwd)":/source:rw,U --env=SOURCE=/source \ 41 | --volume=/usr/lib/os-release:/run/host/usr/lib/os-release:ro \ 42 | "${CONTAINER}" \ 43 | sh /source/test/browser/run-tests.sh 44 | -------------------------------------------------------------------------------- /test/browser/main.fmf: -------------------------------------------------------------------------------- 1 | /main: 2 | summary: Runs all tests 3 | require: 4 | - cockpit 5 | - cockpit-composer 6 | - composer-cli 7 | - nodejs 8 | - git 9 | - curl 10 | - createrepo_c 11 | - dnf-automatic 12 | - firewalld 13 | - git 14 | - libvirt-daemon-config-network 15 | - libvirt-python3 16 | - make 17 | - npm 18 | - python3 19 | - targetcli 20 | - tlog 21 | - podman 22 | test: ./browser.sh 23 | duration: 1h 24 | -------------------------------------------------------------------------------- /test/browser/run-tests.sh: -------------------------------------------------------------------------------- 1 | set -eux 2 | 3 | cd "${SOURCE}" 4 | 5 | # tests need cockpit's bots/ libraries and test infrastructure 6 | git init 7 | rm -f bots # common local case: existing bots symlink 8 | make bots test/common 9 | 10 | # make sure dev dependencies are present so the tests run properly 11 | npm ci 12 | 13 | # disable detection of affected tests; testing takes too long as there is no parallelization 14 | mv .git dot-git 15 | 16 | . /run/host/usr/lib/os-release 17 | export TEST_OS="${ID}-${VERSION_ID/./-}" 18 | 19 | if [ "$TEST_OS" = "centos-9" ]; then 20 | TEST_OS="${TEST_OS}-stream" 21 | fi 22 | 23 | # Chromium sometimes gets OOM killed on testing farm 24 | export TEST_BROWSER=firefox 25 | 26 | # make it easy to check in logs 27 | echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}" 28 | echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}" 29 | 30 | GATEWAY="$(python3 -c 'import socket; print(socket.gethostbyname("_gateway"))')" 31 | RC=0 32 | ./test/common/run-tests \ 33 | --test-dir test/verify \ 34 | --nondestructive \ 35 | --machine "${GATEWAY}":22 \ 36 | --browser "${GATEWAY}":9090 \ 37 | || RC=$? 38 | 39 | echo $RC > "$LOGS/exitcode" 40 | cp --verbose Test* "$LOGS" || true 41 | exit $RC 42 | -------------------------------------------------------------------------------- /test/files/httpd-server-with-hostname.toml: -------------------------------------------------------------------------------- 1 | name = "httpd-server-with-hostname" 2 | description = "httpd server with hostname image" 3 | version = "0.0.2" 4 | modules = [] 5 | groups = [] 6 | 7 | [[packages]] 8 | name = "httpd" 9 | version = "*" 10 | 11 | [[packages]] 12 | name = "tmux" 13 | version = "*" 14 | 15 | [[packages]] 16 | name = "vim-enhanced" 17 | version = "*" 18 | 19 | [customizations] 20 | hostname = "httpd-server-with-hostname" 21 | -------------------------------------------------------------------------------- /test/files/httpd-server-with-user.toml: -------------------------------------------------------------------------------- 1 | name = "httpd-server-with-user" 2 | description = "httpd server with user account image" 3 | version = "0.0.2" 4 | modules = [] 5 | groups = [] 6 | 7 | [[packages]] 8 | name = "httpd" 9 | version = "*" 10 | 11 | [[packages]] 12 | name = "tmux" 13 | version = "*" 14 | 15 | [[packages]] 16 | name = "vim-enhanced" 17 | version = "*" 18 | 19 | [customizations] 20 | 21 | [[customizations.user]] 22 | name = "auser" 23 | description = "admin user" 24 | password = "$6$zehfVoh/POuGYHam$XXAP3RY2ulkHjQNyDrPcosp3P9NT2DRQOxfoRGa7YOE8aaBRl5vxxb2/cE2uaIfhySB/VSxUGgrtg4TsQrLQe1" 25 | groups = ["wheel"] 26 | -------------------------------------------------------------------------------- /test/files/httpd-server.toml: -------------------------------------------------------------------------------- 1 | name = "httpd-server" 2 | description = "httpd server image" 3 | version = "0.0.1" 4 | modules = [] 5 | groups = [] 6 | 7 | [[packages]] 8 | name = "httpd" 9 | version = "*" 10 | 11 | [[packages]] 12 | name = "tmux" 13 | version = "*" 14 | 15 | [[packages]] 16 | name = "vim-enhanced" 17 | version = "*" -------------------------------------------------------------------------------- /test/files/openssh-server.toml: -------------------------------------------------------------------------------- 1 | name = "openssh-server" 2 | description = "ssh server image" 3 | version = "0.0.1" 4 | modules = [] 5 | groups = [] 6 | 7 | [[packages]] 8 | name = "openssh-server" 9 | version = "*" 10 | -------------------------------------------------------------------------------- /test/files/rhel-10.json: -------------------------------------------------------------------------------- 1 | { 2 | "aarch64": [ 3 | { 4 | "name": "baseos", 5 | "baseurl": "http://download.devel.redhat.com/rhel-10/nightly/RHEL-10-Public-Beta/latest-RHEL-10.0/compose/BaseOS/aarch64/os", 6 | "gpgcheck": 0 7 | }, 8 | { 9 | "name": "appstream", 10 | "baseurl": "http://download.devel.redhat.com/rhel-10/nightly/RHEL-10-Public-Beta/latest-RHEL-10.0/compose/AppStream/aarch64/os", 11 | "gpgcheck": 0 12 | } 13 | ], 14 | "x86_64": [ 15 | { 16 | "name": "baseos", 17 | "baseurl": "http://download.devel.redhat.com/rhel-10/nightly/RHEL-10-Public-Beta/latest-RHEL-10.0/compose/BaseOS/x86_64/os", 18 | "gpgcheck": 0 19 | }, 20 | { 21 | "name": "appstream", 22 | "baseurl": "http://download.devel.redhat.com/rhel-10/nightly/RHEL-10-Public-Beta/latest-RHEL-10.0/compose/AppStream/x86_64/os", 23 | "gpgcheck": 0 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/files/rhel-9.6.json: -------------------------------------------------------------------------------- 1 | { 2 | "aarch64": [ 3 | { 4 | "name": "baseos", 5 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.6/compose/BaseOS/aarch64/os", 6 | "gpgcheck": 0 7 | }, 8 | { 9 | "name": "appstream", 10 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.6/compose/AppStream/aarch64/os", 11 | "gpgcheck": 0 12 | } 13 | ], 14 | "x86_64": [ 15 | { 16 | "name": "baseos", 17 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.6/compose/BaseOS/x86_64/os", 18 | "gpgcheck": 0 19 | }, 20 | { 21 | "name": "appstream", 22 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.6/compose/AppStream/x86_64/os", 23 | "gpgcheck": 0 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/files/rhel-95.json: -------------------------------------------------------------------------------- 1 | { 2 | "aarch64": [ 3 | { 4 | "name": "baseos", 5 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.5/compose/BaseOS/aarch64/os", 6 | "gpgcheck": 0 7 | }, 8 | { 9 | "name": "appstream", 10 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.5/compose/AppStream/aarch64/os", 11 | "gpgcheck": 0 12 | } 13 | ], 14 | "x86_64": [ 15 | { 16 | "name": "baseos", 17 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.5/compose/BaseOS/x86_64/os", 18 | "gpgcheck": 0 19 | }, 20 | { 21 | "name": "appstream", 22 | "baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9.5/compose/AppStream/x86_64/os", 23 | "gpgcheck": 0 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/osbuild-mock.repo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osbuild/cockpit-composer/a7b8570c3e16d3f36af0d82b3fce6c020daa0e76/test/osbuild-mock.repo -------------------------------------------------------------------------------- /test/reference-image: -------------------------------------------------------------------------------- 1 | fedora-40 2 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # This is the expected entry point for Cockpit CI; will be called without 3 | # arguments but with an appropriate $TEST_OS, and optionally $TEST_SCENARIO 4 | 5 | TEST_SCENARIO="${TEST_SCENARIO:-}" 6 | [ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox 7 | 8 | # overlays are too big for bot's 10GB /tmp tmpfs 9 | TEST_OVERLAY_DIR="$(pwd)/tmp/run" 10 | mkdir -p $TEST_OVERLAY_DIR 11 | 12 | export RUN_TESTS_OPTIONS=--track-naughties 13 | 14 | make check 15 | 16 | # If successful, report code coverage result to codecov.io 17 | if [ -f ~/.config/codecov-token ]; then 18 | token=$(cat ~/.config/codecov-token | sed 's/\n//g') 19 | curl --silent localhost:8080/foo.sh | CODECOV_TOKEN=$token bash 20 | fi 21 | -------------------------------------------------------------------------------- /test/verify/check-blueprintList: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # checkpoint: 4 | # 1. create blueprint 5 | # 2. blueprint filter 6 | # 3. blueprint sort 7 | 8 | import composerlib 9 | import testlib 10 | 11 | 12 | @testlib.nondestructive 13 | @testlib.no_retry_when_changed 14 | class TestBlueprintList(composerlib.ComposerCase): 15 | 16 | def testFilter(self): 17 | b = self.browser 18 | 19 | self.login_and_go("/composer") 20 | b.wait_visible("#main") 21 | 22 | # filter "openssh-server" blueprint 23 | b.focus("input[aria-label='Blueprints search input']") 24 | b.input_text("openssh") 25 | b.wait_visible("tr[data-testid='openssh-server']") 26 | b.wait_not_present("tr[data-testid='httpd-server']") 27 | # clear filter 28 | b.click(".pf-c-text-input-group__utilities button") 29 | b.wait_visible("tr[data-testid='httpd-server']") 30 | 31 | # filter "httpd" will show three matched blueprints 32 | b.focus("input[aria-label='Blueprints search input']") 33 | b.input_text("httpd") 34 | b.wait_not_present("tr[data-testid='openssh-server']") 35 | b.is_present("tr[data-testid='http-server']") 36 | b.is_present("tr[data-testid='openssh-server-with-hostname']") 37 | b.is_present("tr[data-testid='openssh-server-with-user']") 38 | # clear filter 39 | b.click(".pf-c-text-input-group__utilities button") 40 | b.is_present("tr[data-testid='openssh-server']") 41 | 42 | def testSort(self): 43 | b = self.browser 44 | 45 | self.login_and_go("/composer") 46 | b.wait_visible("#main") 47 | 48 | blueprint_list = [ 49 | "httpd-server", 50 | "httpd-server-with-hostname", 51 | "httpd-server-with-user", 52 | "openssh-server" 53 | ] 54 | # sort from Z-A 55 | b.wait_visible("table[aria-label='Blueprints table']") 56 | b.click("#button-sort-blueprints") 57 | for i, v in enumerate(sorted(blueprint_list, reverse=True)): 58 | b.wait_text("table[aria-label='Blueprints table'] tbody tr:nth-child({}) a".format(i + 1), v) 59 | 60 | # sort from A-Z 61 | b.click("#button-sort-blueprints") 62 | for i, v in enumerate(sorted(blueprint_list)): 63 | b.wait_text("table[aria-label='Blueprints table'] tbody tr:nth-child({}) a".format(i + 1), v) 64 | 65 | 66 | if __name__ == '__main__': 67 | testlib.test_main() 68 | -------------------------------------------------------------------------------- /test/verify/check-imageWizard: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import time 4 | import composerlib 5 | import testlib 6 | import unittest 7 | import os 8 | 9 | 10 | @testlib.nondestructive 11 | @testlib.no_retry_when_changed 12 | class TestImageWizard(composerlib.ComposerCase): 13 | 14 | def _select_PF4(self, selector, value): 15 | b = self.browser 16 | 17 | b.click(f"{selector}:not([disabled]):not([aria-disabled=true])") 18 | select_entry = f"{selector} + ul button:contains('{value}')" 19 | b.click(select_entry) 20 | if b.is_present(f"{selector}.pf-m-typeahead"): 21 | b.wait_val(f"{selector} > div input[type=text]", value) 22 | else: 23 | b.wait_text(f"{selector} .pf-c-select__toggle-text", value) 24 | 25 | def testCreateQCOW2(self): 26 | b = self.browser 27 | 28 | self.login_and_go("/composer", superuser=True) 29 | b.wait_visible("#main") 30 | 31 | # Create blueprint 32 | b.click("tr[data-testid=httpd-server] button[aria-label='Create image']") 33 | b.wait_in_text(".pf-c-wizard__main", "httpd-server") 34 | time.sleep(1) 35 | # select qcow2 image type and keep default size 36 | self._select_PF4("#image-output-select-toggle", "QEMU Image (.qcow2)") 37 | b.click("button:contains('Next')") 38 | 39 | # Create image 40 | b.click("footer button:contains('Create')") 41 | 42 | @unittest.skipIf(os.environ.get("TEST_OS").split('-') != "rhel-95", "Only RHEL-9 supports GCP at this time") 43 | def testUploadGCPFields(self): 44 | b = self.browser 45 | 46 | self.login_and_go("/composer", superuser=True) 47 | b.wait_visible("#main") 48 | 49 | # Select create image from created blueprint 50 | b.click("tr[data-testid=httpd-server] button[aria-label='Create image']") 51 | b.wait_in_text(".pf-c-wizard__main", "httpd-server") 52 | time.sleep(1) 53 | # Google cloud platform image type and keep default size 54 | self._select_PF4("#image-output-select-toggle", "Google Cloud Platform (.tar.gz)") 55 | b.click("input[id='image.isUpload']") 56 | b.click("button:contains('Next')") 57 | 58 | b.set_input_text("input[id='image.upload.image_name']", "testImageName") 59 | b.set_input_text("input[id='image.upload.settings.region']", "testStorageName") 60 | b.set_input_text("input[id='image.upload.settings.bucket']", "testBucket") 61 | b.wait_in_text(".pf-c-wizard__main-body", "Credentials") 62 | 63 | # Cancel upload 64 | b.click("footer button:contains('Cancel')") 65 | 66 | 67 | if __name__ == '__main__': 68 | testlib.test_main() 69 | -------------------------------------------------------------------------------- /test/verify/composerlib.py: -------------------------------------------------------------------------------- 1 | import json 2 | import hashlib 3 | import pathlib 4 | 5 | import parent 6 | import testlib 7 | 8 | # starting osbuild-composer.socket with no permission user will cause error[0-8] 9 | allowed_journal_messages = [ 10 | "http:///run/weldr/api.socket/api/v0/.* couldn't connect: Could not connect: Connection refused", 11 | "polkit-agent-helper-1: pam_authenticate failed: Authentication failure", 12 | "We trust you have received the usual lecture from the local System", 13 | "Administrator. It usually boils down to these three things:", 14 | ".*Respect the privacy of others.*", 15 | ".*Think before you type.*", 16 | ".*With great power comes great responsibility.*", 17 | ".*Sorry, try again.*", 18 | ".*sudo: 3 incorrect password attempts.*", 19 | ] 20 | 21 | allowed_browser_errors = [ 22 | # starting osbuild-composer.socket with no permission user will cause error[0] 23 | ".*Failed to start osbuild-composer.socket.*Not permitted to perform this action.*", 24 | # DDF related 25 | ".*Failed %s type: %s%s prop Invalid prop `FormWrapper` supplied to `FormTemplate`, expected one of type.*", 26 | ".*Failed prop type: Invalid prop `FormWrapper` supplied to `FormTemplate`, expected one of type.*", 27 | ] 28 | 29 | 30 | class ComposerCase(testlib.MachineCase): 31 | def setUp(self): 32 | super().setUp(restrict=False) 33 | 34 | self.allow_journal_messages(*allowed_journal_messages) 35 | self.allow_browser_errors(*allowed_browser_errors) 36 | 37 | # no timeout for image building 38 | self.machine.write("/etc/cockpit/cockpit.conf", "[Session]\nIdleTimeout=0\n") 39 | 40 | # re-start osbuild-composer.socket 41 | self.machine.execute(""" 42 | systemctl stop --quiet osbuild-composer.socket osbuild-composer.service osbuild-local-worker.socket 43 | systemctl start osbuild-composer.socket 44 | """) 45 | 46 | # push pre-defined blueprint 47 | self.machine.execute(""" 48 | for toml_file in /etc/osbuild-composer/blueprints/*.toml; do 49 | composer-cli blueprints push $toml_file 50 | done 51 | """) 52 | 53 | # depsolve one of the blueprints so the repo metadata is cached, this speeds up the tests 54 | self.machine.execute(""" 55 | composer-cli blueprints depsolve httpd-server 56 | """, timeout=900) 57 | 58 | # delete all blueprints 59 | self.addCleanup(self.machine.execute, 60 | "for bp in $(composer-cli blueprints list); " 61 | "do composer-cli blueprints delete $bp; done") 62 | 63 | # Hack to add more wait time to work with aarch64 platform test 64 | def login_and_go(self, *args, **kwargs): 65 | with self.browser.wait_timeout(300): 66 | super().login_and_go(*args, **kwargs) 67 | -------------------------------------------------------------------------------- /test/verify/parent.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pathlib 3 | 4 | TEST_DIR = pathlib.Path(__file__).parent.parent 5 | sys.path.append(str(TEST_DIR / "common")) 6 | sys.path.append(str(TEST_DIR.parent / "bots/machine")) 7 | -------------------------------------------------------------------------------- /test/vm.install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # image-customize script to enable cockpit and osbuild-composer in test VMs 3 | # The application RPM will be installed separately 4 | set -eux 5 | 6 | # on virt-install images with LVM, resize root partition to fill free space 7 | VG=$(vgs --noheadings -o vg_name) 8 | if [ -n "$VG" ]; then 9 | echo -en "n\n\n\n\n\nw\n" | fdisk /dev/vda 10 | pvcreate /dev/vda3 11 | vgextend $VG /dev/vda3 12 | lvextend -r -l +100%FREE $VG/root 13 | fi 14 | 15 | # Repositories in /etc/osbuild-composer/repositories are used only for on-premise 16 | sudo mkdir -p /etc/osbuild-composer/repositories 17 | sudo mkdir -p /etc/osbuild-composer/blueprints 18 | # Copy rhel nightly overrides 19 | if [ -d /home/admin/files ]; then 20 | cp /home/admin/files/rhel-95.json /etc/osbuild-composer/repositories/rhel-95.json 21 | cp /home/admin/files/rhel-9.6.json /etc/osbuild-composer/repositories/rhel-9.6.json 22 | cp /home/admin/files/rhel-10.json /etc/osbuild-composer/repositories/rhel-10.0.json 23 | cp /home/admin/files/*.toml /etc/osbuild-composer/blueprints/ 24 | else 25 | cp test/files/rhel-95.json /etc/osbuild-composer/repositories/rhel-95.json 26 | cp test/files/rhel-9.6.json /etc/osbuild-composer/repositories/rhel-9.6.json 27 | cp test/files/rhel-10.json /etc/osbuild-composer/repositories/rhel-10.0.json 28 | cp test/files/*.toml /etc/osbuild-composer/blueprints/ 29 | fi 30 | ln -s /etc/osbuild-composer/repositories/rhel-95.json /etc/osbuild-composer/repositories/rhel-95-beta.json 31 | ln -s /etc/osbuild-composer/repositories/rhel-95.json /etc/osbuild-composer/repositories/rhel-95-ga.json 32 | 33 | # Allow cockpit port (9090) in INPUT chain 34 | # Do not reload firewall rule during image generation 35 | if type firewall-cmd >/dev/null 2>&1 && firewall-cmd --state > /dev/null 2>&1; then 36 | firewall-cmd --add-service=cockpit --permanent 37 | fi 38 | 39 | # Make cockpit.socket auto-start when system started 40 | systemctl enable --now cockpit.socket 41 | 42 | # Make osbuild-composer.socket auto-start when system started 43 | systemctl enable --now osbuild-composer.socket 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import MiniCssExtractPlugin from "mini-css-extract-plugin"; 3 | 4 | const [mode, devtool] = 5 | process.env.NODE_ENV === "production" 6 | ? ["production", "source-map"] 7 | : ["development", "inline-source-map"]; 8 | 9 | const output = { 10 | path: path.resolve("public"), 11 | filename: "main.js", 12 | sourceMapFilename: "[file].map", 13 | }; 14 | 15 | const plugins = [new MiniCssExtractPlugin()]; 16 | 17 | const config = { 18 | entry: "./src/App.js", 19 | output, 20 | mode, 21 | devtool, 22 | plugins, 23 | externals: { cockpit: "cockpit" }, 24 | resolve: { 25 | modules: ["node_modules"], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.(js|jsx)$/, 31 | include: [path.resolve("src")], 32 | use: { 33 | loader: "babel-loader", 34 | }, 35 | resolve: { fullySpecified: false }, 36 | }, 37 | { 38 | test: /\.css$/, 39 | use: [ 40 | MiniCssExtractPlugin.loader, 41 | { 42 | loader: "css-loader", 43 | options: { url: false }, 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | }; 50 | 51 | export default config; 52 | --------------------------------------------------------------------------------