├── .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 |
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 |
33 |
34 |
35 |
38 |
39 | );
40 |
41 | const Loading = () => (
42 |
43 |
44 |
45 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------