Vendor anaconda-webui and build it into the installer image

Replaces installing a patched anaconda-webui RPM from the forge's package
registry: installer/anaconda-webui/vendor/ is upstream release 86 verbatim
(see its PROVENANCE.md), and installer/Containerfile builds it with
upstream's own `make install` in a native-platform stage, then copies the
result over stock Fedora anaconda-webui. The produced file set matches
Fedora's anaconda-webui-86-1.fc46 exactly. Edits to the installer UI now
happen in vendor/ and ship on the next `task build-installer-iso`, with no
RPM, registry or CI in between.

Also follows bootc-image-builder's rename of --bootc-installer-payload-ref
to --installer-payload-ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Philip Peterson 2026-09-27 05:56:26 -08:00
parent 39de966517
commit 255042fe8d
378 changed files with 494732 additions and 92 deletions

View file

@ -3,16 +3,6 @@ version: '3'
vars: vars:
IMAGE: quine-linux IMAGE: quine-linux
INSTALLER_IMAGE: quine-installer INSTALLER_IMAGE: quine-installer
# dnf .repo for the patched anaconda-webui, built by the fork's CI.
RPM_REPO_URL: https://forge.quinefoundation.com/api/packages/quine/rpm.repo
# Checksum of that repo's current metadata; passed as a build arg so the
# installer's dnf layer is rebuilt exactly when a new RPM gets published.
RPM_REPO_REV:
sh: |
[ -n "{{.RPM_REPO_URL}}" ] || exit 0
base=$(curl -fsSL "{{.RPM_REPO_URL}}" | sed -n 's/^baseurl=//p' | head -1)
sum() { if command -v sha256sum >/dev/null; then sha256sum; else shasum -a 256; fi; }
curl -fsSL "$base/repodata/repomd.xml" 2>/dev/null | sum | cut -c1-16
OUTPUT_DIR: ./output OUTPUT_DIR: ./output
# Any docker-CLI-compatible runtime works for plain builds. Prefers podman, # Any docker-CLI-compatible runtime works for plain builds. Prefers podman,
# falls back to docker; override with CONTAINER_RUNTIME=... (env or task var). # falls back to docker; override with CONTAINER_RUNTIME=... (env or task var).
@ -77,22 +67,19 @@ tasks:
Build the patched-Anaconda installer-environment container (see Build the patched-Anaconda installer-environment container (see
installer/Containerfile and docs/patched-anaconda-installer.md). This installer/Containerfile and docs/patched-anaconda-installer.md). This
is separate from the quine-linux OS image itself. Defaults to the is separate from the quine-linux OS image itself. Defaults to the
anaconda-webui build at {{.RPM_REPO_URL}}; override with anaconda-webui built from installer/anaconda-webui/vendor/.
`task build-installer-image RPM_REPO_URL=...` (empty = stock Fedora).
cmds: cmds:
- sudo {{.CONTAINER_RUNTIME}} build --platform {{.PLATFORM}} -t {{.INSTALLER_IMAGE}} --build-arg RPM_REPO_URL={{.RPM_REPO_URL}} --build-arg RPM_REPO_REV={{.RPM_REPO_REV}} -f installer/Containerfile installer/ - sudo {{.CONTAINER_RUNTIME}} build --platform {{.PLATFORM}} -t {{.INSTALLER_IMAGE}} -f installer/Containerfile installer/
build-installer-iso: build-installer-iso:
desc: >- desc: >-
Build a bootable install ISO using the patched-Anaconda installer Build a bootable install ISO using the patched-Anaconda installer
image to install the quine-linux bootc image, via image to install the quine-linux bootc image, via
bootc-image-builder's `bootc-installer` type. Output lands in bootc-image-builder's `bootc-installer` type. Output lands in
{{.OUTPUT_DIR}}/bootc-installer/install.iso. Uses the same RPM_REPO_URL {{.OUTPUT_DIR}}/bootc-installer/install.iso.
default/override as build-installer-image.
deps: [build-image] deps: [build-image]
cmds: cmds:
- task: build-installer-image - task: build-installer-image
vars: { RPM_REPO_URL: "{{.RPM_REPO_URL}}" }
- task: export-to-storage - task: export-to-storage
vars: { IMAGES: "{{.IMAGE}} {{.INSTALLER_IMAGE}}" } vars: { IMAGES: "{{.IMAGE}} {{.INSTALLER_IMAGE}}" }
- mkdir -p {{.OUTPUT_DIR}} - mkdir -p {{.OUTPUT_DIR}}
@ -104,7 +91,7 @@ tasks:
{{.BIB_IMAGE}} {{.BIB_IMAGE}}
--target-arch {{.TARGET_ARCH}} --target-arch {{.TARGET_ARCH}}
--type bootc-installer --type bootc-installer
--bootc-installer-payload-ref localhost/{{.IMAGE}} --installer-payload-ref localhost/{{.IMAGE}}
localhost/{{.INSTALLER_IMAGE}} localhost/{{.INSTALLER_IMAGE}}
generates: generates:
- "{{.OUTPUT_DIR}}/bootc-installer/install.iso" - "{{.OUTPUT_DIR}}/bootc-installer/install.iso"

View file

@ -11,26 +11,26 @@ custom Anaconda build.
bootc-image-builder's `bootc-installer` type is different: it takes **two** bootc-image-builder's `bootc-installer` type is different: it takes **two**
containers -- an "installer environment" container that has Anaconda containers -- an "installer environment" container that has Anaconda
installed in it, and (via `--bootc-installer-payload-ref`) the actual OS to installed in it, and (via `--installer-payload-ref`) the actual OS to
install. The installer environment is just a container you build with a install. The installer environment is just a container you build with a
`Containerfile`, so that's the hook: build your own installer image with `Containerfile`, so that's the hook: build your own installer image with
your patched Anaconda `dnf install`ed instead of Fedora's, and point your patched Anaconda in it instead of Fedora's, and point
bootc-image-builder at it. bootc-image-builder at it.
`installer/Containerfile` in this repo is that installer environment. It's `installer/Containerfile` in this repo is that installer environment. It's
unrelated to the quine-linux OS image built by the root `Containerfile` -- unrelated to the quine-linux OS image built by the root `Containerfile` --
Anaconda only exists in this side container, never in the installed system. Anaconda only exists in this side container, never in the installed system.
## Which Anaconda to fork ## Which Anaconda to patch
Fedora's `anaconda` package resolves to one of two UIs depending on the Fedora's `anaconda` package resolves to one of two UIs depending on the
image's release-identity package: `anaconda-gui` (old GTK hub-and-spoke) on image's release-identity package: `anaconda-gui` (old GTK hub-and-spoke) on
server/basic/IoT identities, `anaconda-webui` (Cockpit + React/PatternFly) server/basic/IoT identities, `anaconda-webui` (Cockpit + React/PatternFly)
everywhere else. All current upstream feature work happens on the WebUI, so everywhere else. All current upstream feature work happens on the WebUI, so
that's the one to fork: that's the one to patch: https://github.com/rhinstaller/anaconda-webui,
vendored in this repo at `installer/anaconda-webui/vendor/` (see
- https://github.com/rhinstaller/anaconda-webui, forked to `installer/anaconda-webui/PROVENANCE.md` for the pinned release and how to
https://github.com/philip-peterson/quine-anaconda-webui update it).
`installer/Containerfile` depends on `anaconda-webui` explicitly (not just `installer/Containerfile` depends on `anaconda-webui` explicitly (not just
the bare `anaconda` metapackage) so the UI doesn't depend on incidentally the bare `anaconda` metapackage) so the UI doesn't depend on incidentally
@ -38,61 +38,31 @@ matching a release-identity package.
## Workflow ## Workflow
1. **Fork** `rhinstaller/anaconda-webui` to 1. **Edit** the web UI directly in `installer/anaconda-webui/vendor/`
`philip-peterson/quine-anaconda-webui` on GitHub, make your panel/control (mostly `src/`, React/PatternFly). Your changes are this repo's diff
changes there. against the vendored upstream release.
2. **Get it built as an RPM your installer container can `dnf install`.** 2. **Build the installer image and ISO:**
The fork's own Forgejo Actions workflow does this, as a self-hosted
stand-in for COPR: `.forgejo/workflows/rpm.yml` runs
`.forgejo/build-rpm.sh` in a fresh `fedora:rawhide` container (upstream's
`make srpm`, then `rpmbuild`) and uploads the RPM to the forge's package
registry. Setup, once:
- Host the fork on `forge.quinefoundation.com` (Forgejo Actions only runs
for repos on the forge), under the account the package should be
published as -- the `quine` org by default
(`quine/quine-anaconda-webui`).
- Add a repo secret `RPM_UPLOAD_TOKEN`: an access token for that account
with `write:package` scope, from a user
allowed to publish packages for that org.
- The result is served at
`https://forge.quinefoundation.com/api/packages/quine/rpm.repo`,
already wired in as the `RPM_REPO_URL` default in `Taskfile.yml`.
Publishing under a different owner means overriding `RPM_REPO_URL=` on
the `task` invocations below.
Versioning: the build fetches upstream's tags so `git describe` yields
e.g. `86.3.gabcdef0` (3 commits past upstream 86), and the Release is
`<run number>.quine`, so each build is a new version that sorts above
Fedora's own. The installer also gives this repo dnf `priority=10`, so
it wins even when Fedora ships a newer upstream release.
3. **Build the installer image and ISO:**
```sh ```sh
task build-installer-iso task build-installer-iso
``` ```
This builds the quine-linux OS image (if needed), builds `installer/Containerfile` builds the vendored source with upstream's own
`installer/Containerfile` with that repo enabled, and produces `make install` (the same step Fedora's RPM runs), installs stock Fedora
`output/bootc-installer/install.iso`. `RPM_REPO_URL=` (empty) builds `anaconda-webui` for its dependencies, and copies the built files over
with stock Fedora `anaconda-webui` instead. it. The ISO lands in `output/bootc-installer/install.iso`.
4. **Iterate:** push a change to your fork, wait for its RPM workflow, 3. **Iterate:** edit, rerun. The build stage is cached until something in
rerun `task build-installer-iso`. The Taskfile passes a checksum of the `vendor/` changes.
repo's metadata into the build, so a newly published RPM invalidates
the cached `dnf install` layer and gets picked up.
## Local alternative (no forge) The build stage runs on the build host's native architecture (the output is
platform-independent web assets and scripts), and needs network access: it
If you'd rather not publish the fork anywhere: build the RPM locally with fetches upstream's npm package cache and cockpit's shared lib from GitHub.
`mock` targeting the same Fedora release/arch as the installer base,
`createrepo_c` the output into a directory, and either bind-mount that
directory into `installer/Containerfile`'s build context and add it as a
`file://` repo, or drop the built RPMs into a `dnf localinstall` step. This
avoids any external service but means keeping the mock chroot's Fedora
release in sync with the installer base by hand.
## Caveats ## Caveats
- `anaconda-webui`'s package/dependency structure moves with each Fedora - The installer base tracks Fedora rawhide, while `vendor/` is pinned to one
release; if `dnf install anaconda-webui` starts failing, check what upstream release. If rawhide's `anaconda-webui` moves well ahead, its
`anaconda.spec.in` in your fork currently requires. dependencies (e.g. `anaconda-core`) may stop matching the vendored UI;
re-vendor a newer release (see PROVENANCE.md) or pin the base to a
stable Fedora release.
- The installer environment's Fedora release should stay close to whatever - The installer environment's Fedora release should stay close to whatever
release the actual `quine-linux` image (and thus the ISO's kernel/dracut) release the actual `quine-linux` image (and thus the ISO's kernel/dracut)
is built against, to avoid mismatches between the boot environment and the is built against, to avoid mismatches between the boot environment and the

View file

@ -1,31 +1,51 @@
# Installer-environment container for bootc-image-builder's `bootc-installer` # Installer-environment container for bootc-image-builder's `bootc-installer`
# image type. This is NOT the quine-linux OS -- it's the throwaway Anaconda # image type. This is NOT the quine-linux OS -- it's the throwaway Anaconda
# environment the ISO boots into, which then installs the real quine-linux # environment the ISO boots into, which then installs the real quine-linux
# bootc image (passed separately via --bootc-installer-payload-ref). See # bootc image (passed separately via --installer-payload-ref). See
# docs/patched-anaconda-installer.md for the full workflow this fits into. # docs/patched-anaconda-installer.md for the full workflow this fits into.
# Builds the patched anaconda-webui from the vendored source in
# anaconda-webui/vendor/ (see anaconda-webui/PROVENANCE.md) with upstream's
# own `make install`, i.e. exactly the files its RPM would ship. Runs on the
# build host's native platform: the output is platform-independent (web
# assets and scripts), and emulated builds of this stage are both slow and
# unreliable (tar fails under emulation on Apple Silicon).
FROM --platform=$BUILDPLATFORM registry.fedoraproject.org/fedora:rawhide AS anaconda-webui
# Upstream version the vendored source corresponds to, and the gitlink its
# node_modules submodule pinned (the npm package cache, fetched at build).
# Update both when re-vendoring.
ARG ANACONDA_WEBUI_VERSION=86
ARG NODE_MODULES_REF=99a26de1065b0c39bad3d11197a37e1a8b0edb99
RUN dnf install -y git make npm diffutils gettext rpm-build && dnf clean all
COPY anaconda-webui/vendor/ /src/
WORKDIR /src
# The upstream build assumes a git checkout: tools/node-modules reads the
# node_modules gitlink from the index, and the Makefile fetches cockpit's
# shared lib into the repo's object store. Recreate just enough of one.
RUN set -eux; \
git init -q; \
git add -A; \
git update-index --add --cacheinfo "160000,${NODE_MODULES_REF},node_modules"; \
git -c user.name=build -c user.email=build@localhost commit -q -m vendored
RUN set -eux; \
make package-lock.json; \
# node_modules ships only x86_64's esbuild binary; add the host's if needed.
v="$(node -p 'require("./node_modules/esbuild/package.json").version')"; \
case "$(uname -m)" in \
aarch64) npm install --no-save --ignore-scripts "@esbuild/linux-arm64@${v}" ;; \
esac; \
make install DESTDIR=/out VERSION="${ANACONDA_WEBUI_VERSION}"
FROM quay.io/fedora/fedora-bootc:rawhide FROM quay.io/fedora/fedora-bootc:rawhide
# URL of a dnf .repo file for the repository holding a build of the patched # Stock Fedora anaconda-webui pulls in the right dependencies; the patched
# anaconda-webui fork -- by default the Forgejo RPM registry its CI publishes # build is laid over it below.
# to (see docs/patched-anaconda-installer.md). Override at build time with
# --build-arg RPM_REPO_URL=...; empty means stock Fedora anaconda-webui.
ARG RPM_REPO_URL
# Set by the Taskfile to the repo metadata's checksum; only here so a newly
# published RPM invalidates the build cache for the layer below.
ARG RPM_REPO_REV
# priority=10 (default 99) makes dnf take anaconda-webui from that repo even
# when Fedora's own build is newer. Forgejo signs the repo metadata but not
# the RPMs themselves, so verify the signed metadata (which carries each
# package's checksum) instead of per-package signatures.
RUN set -eux; \ RUN set -eux; \
echo "anaconda-webui repo rev: ${RPM_REPO_REV:-none}"; \
if [ -n "${RPM_REPO_URL}" ]; then \
curl -fsSL "${RPM_REPO_URL}" -o /etc/yum.repos.d/quine-anaconda.repo; \
sed -i 's/^gpgcheck=.*/gpgcheck=0/' /etc/yum.repos.d/quine-anaconda.repo; \
printf '\nrepo_gpgcheck=1\npriority=10\n' >> /etc/yum.repos.d/quine-anaconda.repo; \
fi; \
dnf install -y \ dnf install -y \
anaconda-webui \ anaconda-webui \
anaconda \ anaconda \
@ -42,6 +62,10 @@ RUN set -eux; \
prefixdevname; \ prefixdevname; \
dnf clean all dnf clean all
# Replace the packaged web UI wholesale so no stale upstream assets linger.
RUN rm -rf /usr/share/cockpit/anaconda-webui
COPY --from=anaconda-webui /out/ /
# Fedora 43+ path fixup -- image-builder expects the EFI directory under # Fedora 43+ path fixup -- image-builder expects the EFI directory under
# /boot/efi, not wherever the fedora-bootc base puts it. # /boot/efi, not wherever the fedora-bootc base puts it.
RUN mkdir -p /boot/efi && cp -ra /usr/lib/efi/*/*/EFI /boot/efi RUN mkdir -p /boot/efi && cp -ra /usr/lib/efi/*/*/EFI /boot/efi

View file

@ -0,0 +1,44 @@
# Provenance: vendored `anaconda-webui` source
## Origin
- Upstream project: [`rhinstaller/anaconda-webui`](https://github.com/rhinstaller/anaconda-webui)
(Anaconda's Cockpit/React web UI, shipped in Fedora as the `anaconda-webui`
package).
- **Pinned release:** tag `86`, commit
`cd472cd02dec354bb9df463b8724b16bad27202f` ("Show "Installation completed"
instead of "Initializing..." after reboot").
- **Date vendored:** 2026-09-27.
- `vendor/` is `git archive 86` verbatim. Local changes are made directly in
`vendor/` and show up in this repo's history against that baseline.
## Submodules not vendored
Upstream has two git submodules, which `git archive` leaves out:
- `node_modules` -> [`rhinstaller/node-cache`](https://github.com/rhinstaller/node-cache),
gitlink `99a26de1065b0c39bad3d11197a37e1a8b0edb99`. This is the npm package
cache the build needs; upstream's `tools/node-modules` fetches it by that
sha at build time. The sha is recorded as `NODE_MODULES_REF` in
`installer/Containerfile`, which recreates the gitlink before building.
- `test/reference` (pixel-test screenshots): not needed to build.
The build also fetches cockpit's shared `pkg/lib` at the commit pinned in
`vendor/Makefile` (`COCKPIT_REPO_COMMIT`). So building needs network access
to GitHub; it is not offline-capable.
## How it's built and used
`installer/Containerfile`'s `anaconda-webui` stage runs upstream's own
`make install` (what the Fedora RPM's `%install` runs) and the final stage
copies the result over stock Fedora `anaconda-webui`, which is still
installed for its dependencies. Verified at vendoring time: the produced file
set matches Fedora's `anaconda-webui-86-1.fc46` file list exactly.
## Updating to a new upstream release
1. Replace `vendor/` with `git archive <new tag>` of upstream, then re-apply
local changes (diff `vendor/` against the old baseline first to see them).
2. Update `ANACONDA_WEBUI_VERSION` and `NODE_MODULES_REF` in
`installer/Containerfile` (`git ls-tree <new tag> node_modules`).
3. Update the tag, commit and date above.

View file

@ -0,0 +1 @@
ghcr.io/cockpit-project/tasks:2026-09-19

View file

@ -0,0 +1 @@
../test/run

View file

@ -0,0 +1,2 @@
node_modules/*
pkg/lib/*

View file

@ -0,0 +1,64 @@
{
"root": true,
"env": {
"browser": true,
"es2022": true
},
"extends": ["eslint:recommended", "standard", "standard-jsx", "standard-react"],
"parserOptions": {
"ecmaVersion": 2022
},
"plugins": ["react", "react-hooks", "simple-import-sort", "sort-destructure-keys"],
"rules": {
"comma-dangle": "off",
"indent": ["error", 4,
{
"ObjectExpression": "first",
"CallExpression": {"arguments": "first"},
"MemberExpression": 2,
"ignoredNodes": [ "JSXAttribute" ]
}],
"jsx-quotes": ["error", "prefer-double"],
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
"no-console": "error",
"no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }],
"no-var": "error",
"prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }],
"quotes": ["error", "double" ],
"semi": ["error", "always", { "omitLastInOneLineBlock": true }],
"sort-destructure-keys/sort-destructure-keys": 2,
"sort-keys": ["error", "asc", {"caseSensitive": true, "natural": true, "minKeys": 2}],
"simple-import-sort/imports": ["error", { "groups": [
["^cockpit$"],
["^"],
["^react", "^@patternfly"],
["/apis"],
["/actions", "/reducer"],
["/helpers"],
["/components.*js[x]$"],
["/contexts.*js[x]$"],
["/hooks.*js[x]$"],
["^cockpit-", "^os-release", "^dialogs"],
["^\\."],
["\\.scss"]
] }],
"import/extensions": ["error", "ignorePackages", {
"js": "always",
"jsx": "always"
}],
"import/no-useless-path-segments": "error",
"react/hook-use-state": ["error", { "allowDestructuredState": false }],
"react/jsx-closing-bracket-location": "off",
"react/jsx-indent": ["error", 4],
"react/prop-types": "off",
"react-hooks/exhaustive-deps": "error",
"react-hooks/rules-of-hooks": "error"
},
"globals": {
"require": false,
"module": false
}
}

View file

@ -0,0 +1,8 @@
[flake8]
# we want 118, but fix tests first
#max-line-length = 118
max-line-length = 149
# E402: module level import not at top of file; needs updating the tests to current cockpit standard
# W504: line break after binary operator
# E741: ambiguous variable name 'l'
ignore = E402, W504, E741

View file

@ -0,0 +1,2 @@
paths:
- src

View file

@ -0,0 +1,38 @@
version: 2
updates:
# Set update schedule for npm packages.
# FIXME: Update our npm development dependencies monthly.
# See: https://github.com/dependabot/dependabot-core/issues/2390
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
# run these when most of our developers don't work
day: "sunday"
timezone: "Europe/Berlin"
open-pull-requests-limit: 3
commit-message:
prefix: "[no-test]"
labels:
- "node_modules"
groups:
eslint:
patterns:
- "eslint*"
esbuild:
patterns:
- "esbuild*"
stylelint:
patterns:
- "stylelint*"
patternfly:
patterns:
- "@patternfly*"
ignore:
- dependency-name: 'react'
# https://github.com/patternfly/patternfly-react/issues/11345
versions: '>= 19'
# The plugins we use didn't work with anything after 8 at some point
- dependency-name: "eslint"
update-types: ["version-update:semver-major"]

View file

@ -0,0 +1,31 @@
name: cockpit-lib-update
on:
schedule:
- cron: '0 2 * * 4'
# can be run manually on https://github.com/rhinstaller/anaconda-webui/actions
workflow_dispatch:
jobs:
cockpit-lib-update:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
statuses: write
steps:
- name: Set up dependencies
run: |
sudo apt update
sudo apt install -y make
- name: Set up configuration and secrets
run: |
printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig
echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token
- name: Clone repository
uses: actions/checkout@v3
- name: Run cockpit-lib-update
run: |
make bots
bots/cockpit-lib-update

View file

@ -0,0 +1,33 @@
name: CodeQL
on: [push, pull_request]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-22.04
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language:
- javascript
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
config-file: ./.github/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"

View file

@ -0,0 +1,96 @@
name: update node_modules
on:
pull_request:
types: [opened, reopened, synchronize, labeled]
env:
GITHUB_BASE: rhinstaller/anaconda-webui
jobs:
dependabot:
environment: npm-update
permissions:
contents: read
pull-requests: write
timeout-minutes: 5
runs-on: ubuntu-latest
if: ${{ contains(github.event.pull_request.labels.*.name, 'node_modules') }}
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Clear node_modules label
uses: actions/github-script@v9
with:
script: |
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'node_modules'
});
} catch (e) {
if (e.name == 'HttpError' && e.status == 404) {
/* expected: 404 if label is unset */
} else {
throw e;
}
}
- name: Rebase pull request and drop node_modules changes
run: |
set -x
git config --global user.name "GitHub Workflow"
git config --global user.email "cockpituous@cockpit-project.org"
git fetch origin main:main
if ! git rebase main; then
git reset node_modules
GIT_EDITOR=true git rebase --continue
git show --stat
fi
- name: Update node_modules for package.json changes
run: |
make tools/node-modules
git config --global user.name "GitHub Workflow"
git config --global user.email "cockpituous@cockpit-project.org"
eval $(ssh-agent)
ssh-add - <<< '${{ secrets.NODE_CACHE_DEPLOY_KEY }}'
./tools/node-modules install
./tools/node-modules push
git add node_modules
ssh-add -D
ssh-agent -k
- name: Clear [no-test] prefix from PR title
if: ${{ contains(github.event.pull_request.title, '[no-test]') }}
uses: actions/github-script@v9
env:
TITLE: '${{ github.event.pull_request.title }}'
with:
script: |
const title = process.env['TITLE'].replace(/\[no-test\]\W+ /, '')
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
title,
});
- name: Force push node_modules update
run: |
# Dependabot prefixes the commit with [no-test] which we don't want to keep in the commit
title=$(git show --pretty="%s" -s | sed -E "s/\[no-test\]\W+ /infra/")
body=$(git show -s --pretty="%b")
git commit --amend -m "${title}" -m "${body}" --no-edit node_modules
eval $(ssh-agent)
ssh-add - <<< '${{ secrets.SELF_DEPLOY_KEY }}'
git push --force 'git@github.com:${{ github.repository }}' '${{ github.head_ref }}'
ssh-add -D
ssh-agent -k

View file

@ -0,0 +1,55 @@
name: Check Documentation
on:
pull_request:
paths:
- 'src/components/storage/scenarios/**'
- 'src/components/*/**.jsx'
- 'docs/generate-docs.js'
push:
branches: [main]
paths:
- 'src/components/storage/scenarios/**'
- 'src/components/*/**.jsx'
- 'docs/generate-docs.js'
jobs:
check-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Generate documentation
run: make docs
- name: Check for documentation changes
id: check_changes
run: |
if git diff --quiet docs/installation-steps.rst; then
echo "✅ Documentation is up to date"
echo "changes=false" >> $GITHUB_OUTPUT
else
echo "❌ Documentation needs to be updated"
echo "changes=true" >> $GITHUB_OUTPUT
echo ""
echo "=== Documentation changes needed ==="
git diff docs/installation-steps.rst
fi
- name: Fail if documentation is outdated
if: steps.check_changes.outputs.changes == 'true'
run: |
echo "::error::Documentation is outdated. Please run 'make docs' and commit the changes."
exit 1
- name: Success message
if: steps.check_changes.outputs.changes == 'false'
run: |
echo "✅ All documentation is current and up to date!"

View file

@ -0,0 +1,86 @@
name: release
on:
push:
tags:
# this is a glob, not a regexp
- '[0-9]*'
env:
GITHUB_BASE: rhinstaller/anaconda-webui
jobs:
source:
runs-on: ubuntu-latest
container:
image: ghcr.io/cockpit-project/tasks:latest
options: --user root
permissions:
# create GitHub release
contents: write
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install gh
run: dnf install -y gh
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
- name: Pacify git's permission check
run: git config --global --add safe.directory /__w/anaconda-webui/anaconda-webui
- name: Workaround for https://github.com/actions/checkout/pull/697
run: |
TAG_VERSION="$(git describe --tags)"
git fetch --force origin ${TAG_VERSION}:refs/tags/${TAG_VERSION}
- name: Build release
run: make dist
- name: Create a release
run: |
set -x
TAG_VERSION="$(git describe --tags)"
git tag -l --format='%(contents:body)' ${TAG_VERSION} > release-note.txt
REPO_NAME="${GITHUB_REPOSITORY#*/}"
gh release create --title ${TAG_VERSION} --notes-file release-note.txt \
${TAG_VERSION} \
${REPO_NAME}-${TAG_VERSION}.tar.xz
node-cache:
# doesn't depend on it, but let's make sure the build passes before we do this
needs: [source]
runs-on: ubuntu-latest
environment: node-cache
# done via deploy key, token needs no write permissions at all
permissions: {}
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up git
run: |
git config user.name "GitHub Workflow"
git config user.email "cockpituous@cockpit-project.org"
- name: Tag node-cache
run: |
set -eux
# this is a shared repo, prefix with project name
TAG="${GITHUB_REPOSITORY#*/}-$(basename $GITHUB_REF)"
make tools/node-modules
tools/node-modules checkout
cd node_modules
git tag "$TAG"
git remote add cache "ssh://git@github.com/${GITHUB_REPOSITORY%/*}/node-cache"
eval $(ssh-agent)
ssh-add - <<< '${{ secrets.DEPLOY_KEY }}'
# make this idempotent: delete an existing tag
git push cache :"$TAG" || true
git push cache tag "$TAG"
ssh-add -D

View file

@ -0,0 +1,68 @@
name: repository
on:
pull_request_target:
types: [opened, reopened, synchronize, labeled, unlabeled]
env:
GITHUB_BASE: rhinstaller/anaconda-webui
jobs:
check:
name: Protection checks
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
timeout-minutes: 5
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
steps:
- name: Clone target branch
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch PR commits
run: git fetch origin "${BASE_SHA}" "${HEAD_SHA}"
- name: Clear .github-changes label
if: ${{ !endsWith(github.event.action, 'labeled') }}
uses: actions/github-script@v9
with:
script: |
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: '.github-changes'
});
} catch (e) {
if (e.name == 'HttpError' && e.status == 404) {
/* expected: 404 if label is unset */
} else {
throw e;
}
}
- name: Check for .github changes
# We want to run this check any time the .github-changes label is not
# set, which needs to include the case where we just unset it above.
if: ${{ !endsWith(github.event.action, 'labeled') ||
!contains(github.event.pull_request.labels.*.name, '.github-changes') }}
run: |
set -x
git log --full-history --exit-code --patch "${HEAD_SHA}" --not "${BASE_SHA}" -- .github >&2
- name: Check for node_modules availability and package.json consistency
run: |
# Make tools/node-modules available
make tools/node-modules
# for each commit in the PR which modifies package.json or node_modules...
for commit in $(git log --reverse --full-history --format=%H \
"${HEAD_SHA}" --not "${BASE_SHA}" -- package.json node_modules); do
# ... check that package.json and node_modules/.package.json are in sync
tools/node-modules verify "${commit}"
done

View file

@ -0,0 +1,37 @@
name: tasks-container-update
on:
schedule:
- cron: '0 2 * * 1'
# can be run manually on https://github.com/rhinstaller/anaconda-webui/actions
workflow_dispatch:
jobs:
tasks-container-update:
environment: self
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
statuses: write
container:
image: ghcr.io/cockpit-project/tasks
options: --user root
steps:
- name: Set up configuration and secrets
run: |
printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig
mkdir -p ~/.config
echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token
- name: Clone repository
uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
- name: Pacify git's permission check
run: git config --global --add safe.directory /__w/anaconda-webui/anaconda-webui
- name: Run tasks-container-update
run: |
make bots
bots/tasks-container-update

View file

@ -0,0 +1,56 @@
name: test-compose
on:
schedule:
- cron: '0 1 * * *'
# can be run manually on https://github.com/rhinstaller/anaconda-webui/actions
workflow_dispatch:
inputs:
compose_id:
description: 'Compose ID to test (leave empty to test against current event)'
staging:
description: 'Whether to test the staging wiki'
default: 'false'
jobs:
test-compose:
environment: 'fedora-wiki'
permissions:
statuses: write
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Get compose ID
id: get-compose
run: |
if [ -n "${{ github.event.inputs.compose_id }}" ]; then
echo "compose_id=${{ github.event.inputs.compose_id }}" >> $GITHUB_OUTPUT
else
sudo apt-get update
sudo apt-get install -y python3-pip
sudo pip3 install wikitcms
COMPOSE_ID=$(python3 test/fedora-wiki/get_current_compose.py)
echo "compose_id=$COMPOSE_ID" >> $GITHUB_OUTPUT
fi
- name: Trigger and verify compose tests
timeout-minutes: 90
run: |
set -euo pipefail
mkdir -p ~/.config/cockpit-dev
echo "${{ github.token }}" >> ~/.config/cockpit-dev/github-token
export TEST_COMPOSE=${{ steps.get-compose.outputs.compose_id }}
if [ "${{ github.event.inputs.staging }}" = "true" ]; then
make test-compose-staging
else
make test-compose
fi
bots/tests-status - --wait 2>&1 | tee tests-status.log
if grep -q '^ not ok ' tests-status.log; then
echo "Failed test cases:"
grep '^ not ok ' tests-status.log
exit 1
fi

View file

@ -0,0 +1,55 @@
name: validate-wiki-testmap
on:
pull_request:
paths:
- test/*
jobs:
validate:
runs-on: ubuntu-latest
permissions: {}
container:
image: ghcr.io/cockpit-project/tasks
options: --user root
timeout-minutes: 10
steps:
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
- name: Pacify git's permission check
run: git config --global --add safe.directory /__w/anaconda-webui/anaconda-webui
- name: Checkout repository
uses: actions/checkout@v4
- name: Get list of valid test names
id: list_tests
run: |
# Bring ./test/common
make pkg/lib/cockpit-po-plugin.js
./test/common/run-tests -l > valid_tests.txt
echo "tests<<EOF" >> $GITHUB_OUTPUT
cat valid_tests.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Validate JSON entries
run: |
valid_tests=$(echo "${{ steps.list_tests.outputs.tests }}" | tr '\n' ' ')
jq -c 'to_entries[] | .value.tests[]' test/fedora-wiki/wiki-testmap.json | while read -r item; do
name=$(echo "$item" | jq -r '.testname')
url="https://fedoraproject.org/wiki/$(echo "$item" | jq -r '."fedora-wiki-testcase"')"
echo "Validating test case: $name - $url"
# Check if the URL exists
if ! curl --head --silent --fail "$url" > /dev/null; then
echo "❌ URL does not exist: $url"
exit 1
fi
# Check if the name exists in the list of tests
if ! grep -q -w "$name" <<< "$valid_tests"; then
echo "❌ Test name '$name' not found in run-tests output"
exit 1
fi
done

View file

@ -0,0 +1,46 @@
name: weblate-sync-po
on:
schedule:
# Run this on Tuesday evening (UTC), so that it's ready for release on
# Wednesday, with some spare time
- cron: '0 18 * * 2'
# can be run manually on https://github.com/rhinstaller/anaconda-webui/actions
workflow_dispatch:
jobs:
po-refresh:
environment: self
permissions:
pull-requests: write
statuses: write
runs-on: ubuntu-latest
steps:
- name: Set up dependencies
run: |
sudo apt update
sudo apt install -y --no-install-recommends gettext
- name: Clone source repository
uses: actions/checkout@v3
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
path: src
- name: Clone weblate repository
uses: actions/checkout@v3
with:
repository: ${{ github.repository }}-l10n
path: weblate
- name: Copy .po files from weblate repository
run: cp weblate/*.po src/po/
- name: Run po-refresh bot
run: |
cd src
make bots
git config --global user.name "GitHub Workflow"
git config --global user.email "cockpituous@cockpit-project.org"
mkdir -p ~/.config/cockpit-dev
echo ${{ github.token }} >> ~/.config/cockpit-dev/github-token
COVERAGE_THRESHOLD=0 bots/po-refresh

View file

@ -0,0 +1,42 @@
name: weblate-sync-pot
on:
schedule:
# Run this every morning
- cron: '45 2 * * *'
# can be run manually on https://github.com/rhinstaller/anaconda-webui/actions
workflow_dispatch:
jobs:
pot-upload:
environment: anaconda-webui-l10n
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Set up dependencies
run: |
sudo apt update
sudo apt install -y --no-install-recommends npm make gettext appstream
- name: Clone source repository
uses: actions/checkout@v3
with:
path: src
- name: Generate .pot file
run: make -C src po/anaconda-webui.pot
- name: Clone weblate repository
uses: actions/checkout@v3
with:
path: weblate
repository: ${{ github.repository }}-l10n
ssh-key: ${{ secrets.DEPLOY_KEY }}
- name: Commit .pot to weblate repo
run: |
cp src/po/anaconda-webui.pot weblate/anaconda-webui.pot
git config --global user.name "GitHub Workflow"
git config --global user.email "cockpituous@cockpit-project.org"
git -C weblate commit -m "Update source file" -- anaconda-webui.pot
git -C weblate push

View file

@ -0,0 +1,34 @@
# Please keep this file sorted (LC_COLLATE=C.UTF-8),
# grouped into the 3 categories below:
# - general patterns (match in all directories)
# - patterns to match files at the toplevel
# - patterns to match files in subdirs
# general patterns
*.pyc
*.rpm
# toplevel (/...)
/*updates*.img
/Test*.html
/Test*.json
/Test*.log*
/Test*.png
/Test*.xml
/VERSION.txt
/anaconda-webui.spec
/bots
/anaconda-webui-*.tar.xz
/dist
/package-lock.json
/pkg
/tmp/
/tools
# subdirs (/subdir/...)
/po/*.pot
/po/LINGUAS
/test/common/
/test/images
/test/ks*.cfg
/test/report.json

View file

@ -0,0 +1,7 @@
[submodule "test/reference"]
path = test/reference
url = https://github.com/rhinstaller/pixel-test-reference
branch = empty
[submodule "node_modules"]
path = node_modules
url = https://github.com/rhinstaller/node-cache

View file

@ -0,0 +1,33 @@
{
"extends": "stylelint-config-standard-scss",
"rules": {
"at-rule-empty-line-before": null,
"declaration-empty-line-before": null,
"custom-property-empty-line-before": null,
"comment-empty-line-before": null,
"scss/double-slash-comment-empty-line-before": null,
"scss/dollar-variable-colon-space-after": null,
"custom-property-pattern": null,
"declaration-block-no-duplicate-properties": null,
"declaration-block-no-redundant-longhand-properties": null,
"declaration-block-no-shorthand-property-overrides": null,
"declaration-block-single-line-max-declarations": null,
"font-family-no-duplicate-names": null,
"function-url-quotes": null,
"keyframes-name-pattern": null,
"media-feature-range-notation": "prefix",
"no-descending-specificity": null,
"no-duplicate-selectors": null,
"scss/at-extend-no-missing-placeholder": null,
"scss/at-mixin-pattern": null,
"scss/comment-no-empty": null,
"scss/dollar-variable-pattern": null,
"scss/double-slash-comment-whitespace-inside": null,
"scss/load-partial-extension": null,
"scss/no-global-function-names": null,
"scss/operator-no-unspaced": null,
"selector-class-pattern": null,
"selector-id-pattern": null
}
}

View file

@ -0,0 +1,104 @@
Hacking on Anaconda Web UI
==========================
The commands here assume you're in the top level of the Anaconda Web UI git
repository checkout.
Setting up development container
--------------------------------
The anaconda team uses a containerized development environment using toolbx.
If you can install `toolbx <https://containertoolbx.org/>`_ or
`distrobox <https://distrobox.privatedns.org/>`_ on your system, it is highly
recommended to do that:
- It is known to work and gives you reproducible results.
- It avoids having to install development packages on your main machine.
If you are not interested in dealing with containers, just skip this part and continue on the next one::
sudo dnf install toolbox
To create and enter a development toolbx for Anaconda Web UI run this command::
toolbox create --image ghcr.io/cockpit-project/tasks -c anaconda-webui
toolbox enter anaconda-webui
Working on Anaconda Web UI
--------------------------
To prepare Anaconda Web UI sources, you need to run this command::
make dist
The easiest way to test changes you make is to set up a test VM.
You can find intructions for preparing a test VM at ``test/README.rst``.
Running eslint
--------------
Anaconda Web UI uses `ESLint <https://eslint.org/>`_ to automatically check
JavaScript code style in `.js` and `.jsx` files.
ESLint is executed as part of `test/common/static-code`, aka. `make codecheck`.
For developer convenience, the ESLint can be started explicitly by::
npm run eslint
Violations of some rules can be fixed automatically by::
npm run eslint:fix
Rules configuration can be found in the `.eslintrc.json` file.
Running stylelint
------------------
Cockpit uses `Stylelint <https://stylelint.io/>`_ to automatically check CSS code
style in `.css` and `scss` files.
Styleint is executed as part of `test/common/static-code`, aka. `make codecheck`.
For developer convenience, the Stylelint can be started explicitly by::
npm run stylelint
Violations of some rules can be fixed automatically by::
npm run stylelint:fix
Rules configuration can be found in the `.stylelintrc.json` file.
Development with rsync mode
---------------------------
When developing the Web UI, after every change to your sources we need to re-build
and the contents of dist directory need to be copied to the SSH target's
/usr/share/cockpit/anaconda-webui directory.
For automating this, you need to set up the SSH `test-updates` alias,
as described in `<test/README.rst>`_.
Then you can run::
make rsync
Updating `node_modules`
----------------------
Before making any changes, ensure the following environment variable is set::
GITHUB_BASE="rhinstaller/anaconda-webui"
If you need to update package.json — for example, to add or remove a dependency — run::
tools/node-modules install
This command regenerates the node_modules cache based on your updated package.json using npm install.
Note: Your locally rebuilt changes to `node_modules` won't be used by others. A new
version will be created by a GitHub workflow when you open your pull request.
For more details, see the `Cockpit HACKING.md guide <https://github.com/cockpit-project/cockpit/blob/main/HACKING.md#updating-node_modules>`_.

502
installer/anaconda-webui/vendor/LICENSE vendored Normal file
View file

@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Suite 500, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

252
installer/anaconda-webui/vendor/Makefile vendored Normal file
View file

@ -0,0 +1,252 @@
# Copyright (C) 2023 Red Hat, Inc.
# SPDX-License-Identifier: LGPL-2.1-or-later
# stamp file to check if/when npm install ran
# one example file in dist/ to check if that already ran
DIST_TEST=dist/manifest.json
PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' package.json)
RPM_NAME := $(PACKAGE_NAME)
VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.')
TARFILE=$(RPM_NAME)-$(VERSION).tar.xz
SPEC=$(RPM_NAME).spec
# one example file in pkg/lib to check if it was already checked out
COCKPIT_REPO_STAMP=pkg/lib/cockpit-po-plugin.js
# stamp file to check if/when npm install ran
NODE_MODULES_TEST=package-lock.json
ifeq ($(TEST_OS),)
ifdef TEST_COMPOSE
ifneq ($(findstring Rawhide,$(TEST_COMPOSE)),)
TEST_OS=fedora-rawhide-boot
else
TEST_OS=fedora-$(word 2,$(subst -, ,$(TEST_COMPOSE)))-boot
endif
else
TEST_OS=fedora-rawhide-boot
endif
endif
BASE_OS=$(word 1,$(subst -, ,$(TEST_OS)))
RELEASE=$(word 2,$(subst -, ,$(TEST_OS)))
# common arguments for tar, mostly to make the generated tarballs reproducible
TAR_ARGS = --sort=name --mtime "@$(shell git show --no-patch --format='%at')" --mode=go=rX,u+rw,a-s --numeric-owner --owner=0 --group=0
# Anaconda specific variables
ifeq ($(TEST_OS),)
PAYLOAD=fedora-rawhide-anaconda-payload
else
VARIANT=$(shell echo $(TEST_OS) | sed 's/-boot//')
PAYLOAD=$(VARIANT)-anaconda-payload
endif
GITHUB_BASE=rhinstaller/anaconda-webui
UPDATES_IMG=updates.img
TEST_LIVE_OS=fedora-rawhide-live-boot
export GITHUB_BASE
export TEST_OS
#
# i18n
#
LINGUAS=$(basename $(notdir $(wildcard po/*.po)))
po/$(PACKAGE_NAME).js.pot:
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ --language=C --keyword= \
--keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
--keyword=N_ --keyword=NC_:1c,2 \
--keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
--keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
--keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
--from-code=UTF-8 $$(find src/ -name '*.js' -o -name '*.jsx')
po/$(PACKAGE_NAME).html.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
pkg/lib/html2po -o $@ $$(find src -name '*.html')
po/$(PACKAGE_NAME).manifest.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
pkg/lib/manifest2po src/manifest.json -o $@
po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot
msgcat --sort-output --output-file=$@ $^
po/LINGUAS:
echo $(LINGUAS) | tr ' ' '\n' > $@
#
# Build/Install/dist
#
all: $(DIST_TEST)
dist_libexec_SCRIPTS = webui-desktop browser-ext gnome-control-center-ext
# makes sure it gets built as part of `make` and `make dist`
dist_noinst_DATA = \
$(DIST_TEST) \
$(COCKPIT_REPO_STAMP) \
package-lock.json \
package.json \
build.js
VERSION.txt: $(SPEC)
rpmspec -q --queryformat "%{version}\n" $(SPEC) | head -1 > $@
$(SPEC): packaging/$(SPEC).in $(NODE_MODULES_TEST)
provides=$$(npm ls --omit dev --package-lock-only --depth=Infinity | grep -Eo '[^[:space:]]+@[^[:space:]]+' | sort -u | sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /'); \
awk -v p="$$provides" '{gsub(/%{VERSION}/, "$(VERSION)"); gsub(/%{NPM_PROVIDES}/, p)}1' $< > $@
$(DIST_TEST): $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js
$(MAKE) VERSION.txt && \
$(MAKE) package-lock.json && \
NODE_ENV=production ./build.js
watch: VERSION.txt
rm -f dist/*
NODE_ENV=$(NODE_ENV) ESBUILD_WATCH=true ./build.js
rsync:
RSYNC=$${RSYNC:-test-updates} make watch
# Generate comprehensive installation documentation (includes steps and storage scenarios)
# NOTE: Run this and commit changes when modifying UI components - CI will check if docs are current
.PHONY: docs
docs:
node docs/generate-docs.js
@echo "Installation documentation generated in docs/"
install: $(DIST_TEST) po/LINGUAS
mkdir -p $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME)
cp -r dist/* $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME)
mkdir -p $(DESTDIR)/usr/share/anaconda
cp -r firefox-theme $(DESTDIR)/usr/share/anaconda/
mkdir -p $(DESTDIR)/usr/share/applications
cp extlinks.desktop $(DESTDIR)/usr/share/applications/
cp anaconda-gnome-control-center.desktop $(DESTDIR)/usr/share/applications/
mkdir -p $(DESTDIR)/usr/libexec/anaconda
cp webui-desktop $(DESTDIR)/usr/libexec/anaconda
cp browser-ext $(DESTDIR)/usr/libexec/anaconda
cp gnome-control-center-ext $(DESTDIR)/usr/libexec/anaconda
cp src/scripts/cockpit-coproc-wrapper.sh $(DESTDIR)/usr/libexec/anaconda/
cp src/scripts/cockpit-pin-auth $(DESTDIR)/usr/libexec/anaconda/
cp src/scripts/anaconda-cockpit-conf-merge $(DESTDIR)/usr/libexec/anaconda/
mkdir -p $(DESTDIR)/usr/lib/systemd/system/
cp src/systemd/webui-cockpit-ws.service $(DESTDIR)/usr/lib/systemd/system/
cp src/systemd/cockpit-pin-auth@.service $(DESTDIR)/usr/lib/systemd/system/
cp src/systemd/cockpit-pin-auth.socket $(DESTDIR)/usr/lib/systemd/system/
cp src/systemd/webui-tls-proxy.service $(DESTDIR)/usr/lib/systemd/system/
cp src/scripts/webui-tls-proxy $(DESTDIR)/usr/libexec/anaconda/
mkdir -p $(DESTDIR)/etc/anaconda/cockpit/conf.d/
cp src/config/cockpit/cockpit.conf $(DESTDIR)/etc/anaconda/cockpit/cockpit.conf
# Staged config, not active by default — webui-desktop copies it to conf.d/ at runtime
mkdir -p $(DESTDIR)/usr/share/anaconda/cockpit/conf.d/
cp src/config/cockpit/conf.d/50-remote-auth.conf $(DESTDIR)/usr/share/anaconda/cockpit/conf.d/
dist: $(TARFILE)
@ls -1 $(TARFILE)
# when building a distribution tarball, call bundler with a 'production' environment
# we don't ship most node_modules for license and compactness reasons, only the ones necessary for running tests
# we ship a pre-built dist/ (so it's not necessary) and ship package-lock.json (so that node_modules/ can be reconstructed if necessary)
$(TARFILE): export NODE_ENV ?= production
$(TARFILE): $(DIST_TEST) $(SPEC)
tar --xz $(TAR_ARGS) -cf $(TARFILE) --transform 's,^,$(RPM_NAME)/,' \
--exclude '*.in' --exclude test/reference \
$$(git ls-files | grep -v node_modules) \
$(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) $(SPEC) VERSION.txt \
dist/
srpm: $(TARFILE) $(SPEC)
rpmbuild -bs \
--define "_sourcedir `pwd`" \
--define "_srcrpmdir `pwd`" \
$(SPEC)
EXTRA_DIST = dist src firefox-theme
# checkout common files from Cockpit repository required to build this project;
# this has no API stability guarantee, so check out a stable tag when you start
# a new project, use the latest release, and update it from time to time
COCKPIT_REPO_FILES = \
pkg/lib \
test/common \
tools/node-modules \
$(NULL)
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
COCKPIT_REPO_COMMIT = 853699ab9697053d299921db46a5900c7a23649e # 368
$(COCKPIT_REPO_FILES): $(COCKPIT_REPO_STAMP)
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
$(COCKPIT_REPO_STAMP): Makefile
@git rev-list --quiet --objects $(COCKPIT_REPO_TREE) -- 2>/dev/null || \
git fetch --no-tags --no-write-fetch-head --depth=1 $(COCKPIT_REPO_URL) $(COCKPIT_REPO_COMMIT)
git archive $(COCKPIT_REPO_TREE) -- $(COCKPIT_REPO_FILES) | tar x
.PHONY: codecheck
codecheck: test/common $(NODE_MODULES_TEST)
test/common/static-code
# checkout Cockpit's bots for standard test VM images and API to launch them
# must be from main, as only that has current and existing images; but testvm.py API is stable
# support CI testing against a bots change
# Workaround cockpit's expectation for test/images directory. This is not really needed in our case
# as we consume ready ISOs
# https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py#L1118
bots: test/common
test/common/make-bots
cd test && ln -sf ../bots/images images
live-vm: bots $(UPDATES_IMG)
./test/webui_testvm.py $(TEST_LIVE_OS)
prepare-test-deps: bots test/common test/reference payload images
.PHONY: payload
payload: bots
bots/image-download $(PAYLOAD)
@echo "Unpacking $(PAYLOAD) to tmp for local serving"
@rm -rf tmp/$(PAYLOAD) && mkdir -p tmp/$(PAYLOAD)
@tar -C tmp/$(PAYLOAD) -xzf bots/images/$(PAYLOAD)
@echo "Payload unpacked into tmp/$(PAYLOAD)"
.PHONY: images
images: bots
# Download ISO images for testing a compose
@if [ -n "$(TEST_COMPOSE)" ]; then \
test/download-iso "$(TEST_OS)" "$(TEST_COMPOSE)" "$(RELEASE)"; \
fi
bots/image-download "$(TEST_OS)"
$(UPDATES_IMG): prepare-test-deps
test/prepare-updates-img
create-updates.img: bots
-rm *updates.img
make $(UPDATES_IMG)
test/reference: test/common
test/common/pixel-tests pull
update-reference-images: test/common test/reference
test/common/pixel-tests push
# We want tools/node-modules to run every time package-lock.json is requested
# See https://www.gnu.org/software/make/manual/html_node/Force-Targets.html
FORCE:
$(NODE_MODULES_TEST): FORCE tools/node-modules
tools/node-modules make_package_lock_json
.PHONY: print-test-os
print-test-os:
@echo $(TEST_OS)
.PHONY: test-compose
test-compose: bots
bots/tests-trigger --force "-" "${TEST_OS}/compose-${TEST_COMPOSE}"
bots/tests-trigger --force "-" "${TEST_OS}/bios-compose-${TEST_COMPOSE}"
.PHONY: test-compose-staging
test-compose-staging: bots
bots/tests-trigger --force "-" "${TEST_OS}/compose-${TEST_COMPOSE}-staging"
bots/tests-trigger --force "-" "${TEST_OS}/bios-compose-${TEST_COMPOSE}-staging"

View file

@ -0,0 +1,46 @@
Anaconda Web UI
===============
Getting the source
------------------
Here's where to get the code::
git clone https://github.com/rhinstaller/anaconda-webui.git
cd anaconda-webui
Development instructions
------------------------
See `<HACKING.rst>`_ and `<test/README.rst>`_ for details about how to efficiently change the code,
run, and test it.
Automated release
-----------------
The intention is that the only manual step for releasing a project is to create
a signed tag for the version number, which includes a summary of the noteworthy
changes::
123
- this new feature
- fix bug #123
Pushing the release tag triggers the `release.yml <.github/workflows/release.yml>`_
`GitHub action <https://github.com/features/actions>`_ workflow. This creates the
official release tarball and publishes as upstream release to GitHub.
The Fedora and COPR releases are done with `Packit <https://packit.dev/>`_.
see the `packit.yaml <./packit.yaml>`_ control file.
Automated maintenance
---------------------
It is important to keep your `NPM modules <./package.json>`_ up to date, to keep
up with security updates and bug fixes. This is done with dependabot, see the
`dependabot.yml <./.github/dependabot.yml>`_ control file.
Similarly, translations are refreshed every Tuesday evening (or manually) through the
`weblate-sync-po.yml <.github/workflows/weblate-sync-po.yml>`_ action.
Conversely, the PO template is uploaded to weblate every day through the
`weblate-sync-pot.yml <.github/workflows/weblate-sync-pot.yml>`_ action.

View file

@ -0,0 +1,16 @@
[Desktop Entry]
Version=1.0
Name=Keyboard External
Exec=/usr/libexec/anaconda/gnome-control-center-ext keyboard
Terminal=false
Type=Application
MimeType=x-scheme-handler/anaconda-gnome-control-center;
StartupNotify=true
Categories=GNOME;GTK;Settings;HardwareSettings;
Actions=Region;
NoDisplay=true
X-Desktop-File-Install-Version=0.26
[Desktop Action Region]
Name=Region & Language
Exec=/usr/libexec/anaconda/gnome-control-center-ext keyboard

40
installer/anaconda-webui/vendor/browser-ext vendored Executable file
View file

@ -0,0 +1,40 @@
#!/bin/bash
#
# Copyright (C) 2025 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This should be eventually get shared across projects:
# https://issues.redhat.com/browse/INSTALLER-3755
# Get the default browser for the 'https' protocol
DEFAULT_BROWSER=$(xdg-mime query default x-scheme-handler/https)
URL="${1/extlink/https}"
# If Firefox is the default browser, use the custom profile
if [[ "$DEFAULT_BROWSER" == "org.mozilla.firefox.desktop" ]]; then
# Create a temporary Firefox profile
PROFILE_DIR="/tmp/anaconda-extlink-firefox-profile"
mkdir -p "$PROFILE_DIR"
# Copy the custom user.js for the Firefox settings
cp /usr/share/anaconda/firefox-theme/extlink/user.js "$PROFILE_DIR/"
# Launch Firefox with the custom profile
/usr/bin/firefox --profile "$PROFILE_DIR" --no-remote "$URL"
else
# For any other browser, use xdg-open
xdg-open "$URL"
fi

124
installer/anaconda-webui/vendor/build.js vendored Executable file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env node
import esbuild from "esbuild";
import copy from "esbuild-plugin-copy";
import { sassPlugin } from "esbuild-sass-plugin";
import fs from "fs";
import path from "path";
import { cockpitPoEsbuildPlugin } from "./pkg/lib/cockpit-po-plugin.js";
import { cockpitRsyncEsbuildPlugin } from "./pkg/lib/cockpit-rsync-plugin.js";
import { cleanPlugin } from "./pkg/lib/esbuild-cleanup-plugin.js";
import { cockpitCompressPlugin } from "./pkg/lib/esbuild-compress-plugin.js";
const production = process.env.NODE_ENV === "production";
const watchMode = process.env.ESBUILD_WATCH === "true" || false;
/* List of directories to use when resolving import statements */
const nodePaths = ["pkg/lib"];
const outdir = "dist";
// Obtain package name from package.json
const packageJson = JSON.parse(fs.readFileSync("package.json"));
const getTime = () => new Date().toTimeString()
.split(" ")[0];
// similar to fs.watch(), but recursively watches all subdirectories
function watchDirs (dir, onChange) {
const callback = (ev, dir, fname) => {
// only listen for "change" events, as renames are noisy
// ignore hidden files and the "4913" temporary file created by vim
const isHidden = /^\./.test(fname);
if (ev !== "change" || isHidden || fname === "4913") {
return;
}
onChange(path.join(dir, fname));
};
fs.watch(dir, {}, (ev, path) => callback(ev, dir, path));
// watch all subdirectories in dir
const d = fs.opendirSync(dir);
let dirent;
while ((dirent = d.readSync()) !== null) {
if (dirent.isDirectory()) {
watchDirs(path.join(dir, dirent.name), onChange);
}
}
d.closeSync();
}
const context = await esbuild.context({
bundle: true,
entryPoints: ["./src/index.js", "./src/anacondaLogin.js"],
external: ["*.woff", "*.woff2", "*.jpg", "*.svg", "../../assets*"],
legalComments: "external", // Move all legal comments to a .LEGAL.txt file
loader: {
".js": "jsx",
".py": "text",
".txt": "text",
},
minify: production,
nodePaths,
outdir,
plugins: [
cleanPlugin(),
// Esbuild will only copy assets that are explicitly imported and used
// in the code. This is a problem for index.html and manifest.json which are not imported
copy({
assets: [
{ from: ["./images/qr-code-feedback.svg"], to: ["./qr-code-feedback.svg"] },
{ from: ["./src/manifest.json"], to: ["./manifest.json"] },
{ from: ["./src/index.html"], to: ["./index.html"] },
{ from: ["./src/login.html"], to: ["./login.html"] },
{ from: ["./VERSION.txt"], to: ["./VERSION.txt"] },
]
}),
sassPlugin({
loadPaths: [...nodePaths, "node_modules"],
quietDeps: true,
}),
cockpitPoEsbuildPlugin(),
// Login assets are served via /cockpit/static/ which does not
// support .gz transparent serving, causing MIME type mismatch.
cockpitCompressPlugin({ exclude: /anacondaLogin\.(js|css)$/ }),
cockpitRsyncEsbuildPlugin({ dest: packageJson.name }),
{
name: "notify-end",
setup (build) {
build.onEnd(() => {
// eslint-disable-next-line no-console
console.log(`${getTime()}: Build finished`);
});
}
},
],
sourcemap: "linked",
target: ["es2020"],
});
try {
await context.rebuild();
} catch (e) {
if (!watchMode) {
process.exit(1);
}
// ignore errors in watch mode
}
if (watchMode) {
const onChange = async path => {
// eslint-disable-next-line no-console
console.log("change detected:", path);
await context.cancel();
try {
await context.rebuild();
} catch (e) {} // ignore in watch mode
};
watchDirs("src", onChange);
// wait forever until Control-C
await new Promise(() => {});
}
context.dispose();

View file

@ -0,0 +1,40 @@
Documentation Generation System
=================================
Overview
--------
This system automatically generates comprehensive documentation for Anaconda
Web UI. The generated documentation is ready for readthedocs.io integration and
includes complete information about the installation flow.
Usage
-----
Generate Documentation
~~~~~~~~~~~~~~~~~~~~~~~
To generate/update all documentation:
.. code-block:: bash
make docs
This generates:
- ``docs/installation-steps.rst`` - Complete installation guide including all pages and storage scenarios
Continuous Integration
----------------------
A GitHub Action (``.github/workflows/docs-check.yml``) automatically verifies that documentation stays up-to-date with code changes.
Contributing
------------
When adding new scenarios or pages:
1. Add JSDoc ``@description`` comments in page or scenario file
2. Run ``make docs`` to update documentation
CI will automatically verify that documentation stays current with code changes.

View file

@ -0,0 +1,176 @@
Customizing the Installer Web UI
=================================
This document describes how distributions and derivatives (such as Bazzite and Bluefin) can customize the Installer Web UI to match their branding and requirements.
Customization can be broadly categorized into two areas:
- **Branding and chroming** - Changing the visual appearance and naming without changing behavior (see :ref:`branding-and-chroming`)
- **Changing installer behavior** - Modifying defaults and functionality through configuration files (see :ref:`changing-installer-behavior`)
These customizations can be deployed in different ways:
- **Build-time customization**: Changes are part of the image when building your own images or using image building tools (e.g., Image Builder)
- **Runtime customization**: Changes are provided with a product image at runtime when using existing images (e.g., official RHEL boot ISO or official Fedora boot ISO)
.. _branding-and-chroming:
Branding and Chroming the User Interface
-----------------------------------------
The Installer Web UI can be customized to match your distribution's visual identity through branding. Cockpit provides branding files per distribution variant, located in the `Cockpit repository <https://github.com/cockpit-project/cockpit/tree/main/src/branding/>`_. The Installer Web UI utilizes these Cockpit branding files and adds anaconda-specific branding through the ``.anaconda`` CSS class wrapper in your distribution's Cockpit ``branding.css`` file.
We propose using the following template, which defines three brand color variables:
- ``--brand-default`` - Primary brand color (used in light theme and hover states in dark theme)
- ``--brand-default-light`` - Lighter variant (used in dark theme)
- ``--brand-default-dark`` - Darker variant (used for hover states in light theme)
Example branding CSS:
.. code-block:: css
.anaconda {
/* Brand palette */
--brand-default-light: #51a2da;
--brand-default: #3c6eb4;
--brand-default-dark: #294172;
.logo {
background-image: url("logo.png");
}
}
:not(.pf-v6-theme-dark) .anaconda {
/* Default (light theme) token mapping */
--pf-t--global--color--brand--default: var(--brand-default);
--pf-t--global--color--brand--hover: var(--brand-default-dark);
}
.pf-v6-theme-dark .anaconda {
/* Dark theme token mapping */
--pf-t--global--color--brand--default: var(--brand-default-light);
--pf-t--global--color--brand--hover: var(--brand-default);
}
The CSS file is imported as-is, so you can add whatever customizations you want through CSS.
Logos are automatically detected and symlinked from system directories during package installation. You only need to reference ``logo.png`` in your CSS using ``background-image: url("logo.png")``. The build system will create the appropriate symlinks.
See the `Fedora branding example <https://github.com/cockpit-project/cockpit/blob/main/src/branding/fedora/branding.css>`_ for a complete reference implementation that you can use as a template.
.. _changing-installer-behavior:
Changing installer behavior through configuration files
-------------------------------------------------------
The installer's behavior and defaults can be customized through Anaconda configuration files. For complete documentation on Anaconda configuration files, see the `Anaconda configuration files documentation <https://anaconda-installer.readthedocs.io/en/latest/developer/configuration-files.html>`_.
Hiding Web UI pages
^^^^^^^^^^^^^^^^^^^^
The Installer Web UI allows you to hide specific installation pages that may not be relevant for your distribution or ISO variant. This is useful when certain steps are handled elsewhere (e.g., by first-boot tools like GNOME Initial Setup).
Pages are hidden using the ``hidden_webui_pages`` option in the ``[User Interface]`` section of configuration files.
Example configuration:
.. code-block:: ini
[User Interface]
hidden_webui_pages =
anaconda-screen-accounts
anaconda-screen-date-time
Available page IDs
''''''''''''''''''
The following page IDs can be used with ``hidden_webui_pages``:
- ``anaconda-screen-language`` - Installation language and keyboard selection
- ``anaconda-screen-date-time`` - Date, time, and timezone configuration
- ``anaconda-screen-software-selection`` - Package environment selection
- ``anaconda-screen-accounts`` - User account and root password creation
Example use case
''''''''''''''''
In Fedora Workstation, the account creation page is hidden because user account setup is handled by GNOME Initial Setup during first boot. The date and time page is also hidden as it's configured automatically. See `anaconda/data/profile.d/fedora-workstation.conf <https://github.com/rhinstaller/anaconda/blob/main/data/profile.d/fedora-workstation.conf>`_ for a complete example.
Default web browser
^^^^^^^^^^^^^^^^^^^
The default browser is set using the ``webui_web_engine`` option in the ``[User Interface]`` section of configuration files.
Example configuration:
.. code-block:: ini
[User Interface]
# The default browser for the Web UI.
webui_web_engine = firefox
The value can be either an executable name available in ``$PATH`` (e.g., ``firefox``) or an absolute path to the executable (e.g., ``/usr/bin/firefox``).
**Important**: When changing the default browser, ensure that the specified browser is available in the installation environment. This typically means adding the browser package to your custom Live image or boot ISO.
Example use case
''''''''''''''''
The default browser for Fedora is `Slitherer <https://gitlab.com/VelocityLimitless/Projects/slitherer>`_. Fedora Workstation overrides this to use Firefox, while Fedora KDE uses Slitherer (inherited from the base Fedora profile). See the respective profile files:
- `anaconda/data/profile.d/fedora-workstation.conf <https://github.com/rhinstaller/anaconda/blob/main/data/profile.d/fedora-workstation.conf>`_
- `anaconda/data/profile.d/fedora.conf <https://github.com/rhinstaller/anaconda/blob/main/data/profile.d/fedora.conf>`_
.. _cockpit-web-service-configuration:
Cockpit Web Service Configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Installer Web UI runs on Cockpit's web service (``cockpit-ws``). Its configuration lives under an Anaconda-owned path at ``/etc/anaconda/cockpit/`` instead of the default ``/etc/cockpit/`` to prevent installer-specific settings (such as disabled TLS or authentication) from leaking into the installed system.
The base configuration file is ``/etc/anaconda/cockpit/cockpit.conf``. It uses the standard `cockpit.conf(5) <https://cockpit-project.org/guide/latest/cockpit.conf.5.html>`_ INI format.
Default settings:
.. code-block:: ini
[WebService]
AllowUnencrypted = true
LoginTo = false
AllowMultiHost = false
[Session]
IdleTimeout = 0
conf.d drop-in overrides
'''''''''''''''''''''''''
To customize the Cockpit web service for specific environments without modifying the base configuration, place ``*.conf`` files in ``/etc/anaconda/cockpit/conf.d/``. Files are applied in lexicographic order, and later files override values from earlier ones and from the base configuration.
Example -- enable TLS for a network-based installation:
.. code-block:: ini
# /etc/anaconda/cockpit/conf.d/50-tls.conf
[WebService]
AllowUnencrypted = false
Example -- set a custom login title:
.. code-block:: ini
# /etc/anaconda/cockpit/conf.d/50-branding.conf
[WebService]
LoginTitle = My Custom Installer
The merged configuration is written to ``/run/anaconda/cockpit/cockpit.conf`` at service start and is used by ``cockpit-ws`` via the ``XDG_CONFIG_DIRS`` environment variable.
For the full list of available cockpit.conf options, see the `cockpit.conf(5) man page <https://cockpit-project.org/guide/latest/cockpit.conf.5.html>`_.
Future: Add-on Support
-----------------------
Add-on support for the Installer Web UI is planned but not yet implemented. This will allow extending the Web UI with custom functionality.
For tracking this feature, see `INSTALLER-2958 <https://issues.redhat.com/browse/INSTALLER-2958>`_.

View file

@ -0,0 +1,416 @@
#!/usr/bin/env node
/*
* Auto-generate documentation for Anaconda storage scenarios
* This script extracts @description from JSDoc comments in scenario files
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Mock cockpit dependency for Node.js environment
global.cockpit = {
gettext: (text) => text,
locale: () => "en"
};
// eslint-disable-next-line no-console
const log = console.log;
// eslint-disable-next-line no-console
const warn = console.warn;
// Utility function to find all index.js files in a directory recursively
function findIndexFiles (dir) {
const files = [];
function traverse (currentDir) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
traverse(fullPath);
} else if (entry.name === "index.js") {
files.push(fullPath);
}
}
}
traverse(dir);
return files;
}
// Global mocking system - create mocked versions of all files once
class MockingSystem {
constructor () {
this.mockedFiles = new Map();
this.tempDir = path.join(__dirname, ".temp-mocks");
}
async initialize () {
// Create temp directory
if (fs.existsSync(this.tempDir)) {
fs.rmSync(this.tempDir, { recursive: true });
}
fs.mkdirSync(this.tempDir, { recursive: true });
// Mock all index.js files that import JSX
await this.mockAllIndexFiles();
// Mock steps.js
await this.mockStepsFile();
}
cleanup () {
if (fs.existsSync(this.tempDir)) {
fs.rmSync(this.tempDir, { recursive: true });
}
}
async mockAllIndexFiles () {
// Find all index.js files in component directories
const srcDir = path.resolve(__dirname, "../src/components");
const indexFiles = findIndexFiles(srcDir);
for (const filePath of indexFiles) {
await this.createMockedFile(filePath);
}
}
async mockScenarioFiles (scenarioFiles) {
// Mock scenario files that aren't already mocked
for (const filePath of scenarioFiles) {
if (!this.mockedFiles.has(filePath)) {
await this.createMockedFile(filePath);
}
}
}
async createMockedFile (originalPath) {
let content = fs.readFileSync(originalPath, "utf8");
// Apply comprehensive mocking
content = this.applyMocking(content);
// Create relative path for temp file
const relativePath = path.relative(path.resolve(__dirname, "../src"), originalPath);
const mockedPath = path.join(this.tempDir, relativePath);
// Ensure directory exists
fs.mkdirSync(path.dirname(mockedPath), { recursive: true });
// Write mocked file
fs.writeFileSync(mockedPath, content);
// Store mapping
this.mockedFiles.set(originalPath, mockedPath);
return mockedPath;
}
async mockStepsFile () {
const stepsPath = path.resolve(__dirname, "../src/components/steps.js");
let content = fs.readFileSync(stepsPath, "utf8");
// Apply basic mocking for steps.js
content = this.applyMocking(content);
// Update imports to point to mocked files
content = content.replace(
/import\s+\{\s*Page\s+as\s+([^}]+)\s*\}\s+from\s+["']([^"']+)["'];?/g,
(match, pageName, importPath) => {
// Convert relative import to absolute
const absoluteImportPath = path.resolve(path.dirname(stepsPath), importPath);
const mockedPath = this.mockedFiles.get(absoluteImportPath);
const relativeToTemp = path.relative(path.dirname(path.join(this.tempDir, "components/steps.js")), mockedPath);
return `import { Page as ${pageName} } from "./${relativeToTemp}";`;
}
);
// Write mocked steps.js
const mockedStepsPath = path.join(this.tempDir, "components/steps.js");
fs.mkdirSync(path.dirname(mockedStepsPath), { recursive: true });
fs.writeFileSync(mockedStepsPath, content);
return mockedStepsPath;
}
applyMocking (content) {
// Mock cockpit import
content = content.replace(
/import\s+cockpit\s+from\s+["']cockpit["'];?/g,
"const cockpit = { gettext: (text) => text, format: (str, ...args) => str };"
);
// Mock debug import
content = content.replace(
/import\s+\{\s*debug\s*\}\s+from\s+["'][^"']*["'];?/g,
"const debug = () => {};"
);
// Mock all JSX file imports with comprehensive function mocks
content = content.replace(
/import\s+\{\s*([^}]+)\s*\}\s+from\s+["'][^"']*\.jsx["'];?/g,
(match, componentNames) => {
const names = componentNames.split(",").map(name => name.trim())
.filter(name => name.length > 0);
const mocks = names.map(name => {
// Create different types of mocks based on naming patterns
if (name.startsWith("use") || name.startsWith("get")) {
return `const ${name} = () => null;`;
} else {
return `const ${name} = {};`;
}
}).join("\n");
return mocks;
}
);
return content;
}
async importMockedSteps () {
const mockedStepsPath = path.join(this.tempDir, "components/steps.js");
const moduleUrl = "file://" + mockedStepsPath;
const module = await import(moduleUrl);
return module.getSteps;
}
async importMockedScenario (originalPath) {
const mockedPath = this.mockedFiles.get(originalPath);
if (!mockedPath) {
throw new Error(`No mocked version found for ${originalPath}`);
}
const moduleUrl = "file://" + mockedPath;
const module = await import(moduleUrl);
return module;
}
}
// Get step order from the getSteps function
async function getOrderedSteps (mockingSystem) {
const getSteps = await mockingSystem.importMockedSteps();
// Call getSteps with mock arguments
const steps = getSteps({}, {}, {});
return steps
.flat()
.filter(step => step.id && step._description);
}
// Extract scenario information using the mocking system
async function extractScenarioInfo (indexPath, mockingSystem) {
if (!fs.existsSync(indexPath)) {
return null;
}
const content = fs.readFileSync(indexPath, "utf8");
// Extract JSDoc comments
const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//);
let description = null;
if (jsdocMatch) {
const jsdocContent = jsdocMatch[1];
// Extract description
const descMatch = jsdocContent.match(/@description\s+(.*?)(?=@|$)/s);
if (descMatch) {
description = descMatch[1]
.replace(/\s*\*\s*/g, " ")
.replace(/\s+/g, " ")
.trim();
}
}
// Use the pre-mocked file from the mocking system
const module = await mockingSystem.importMockedScenario(indexPath);
if (!module.scenario) {
warn(`No scenario export found in ${indexPath}`);
return null;
}
return {
description,
...module.scenario,
};
}
// Extract page information using the mocking system
async function extractPageInfo (filePath, mockingSystem) {
// Use the pre-mocked file from the mocking system
const module = await mockingSystem.importMockedScenario(filePath);
// Check if this file has a Page class, and if so extract page info
if (module.Page) {
// Create instance to get properties (pass dummy args for constructors that expect them)
const pageInstance = new module.Page(false, "erase-all");
const result = {
description: pageInstance._description || null,
...pageInstance,
};
return result;
}
return null; // No page definition in this file
}
// Generate dynamic flow overview from page data
function generateFlowOverview (orderedSteps) {
let overview = "";
orderedSteps.forEach((step, index) => {
const stepNumber = index + 1;
overview += `${stepNumber}. **${!step.isFinal ? step.label : "Installation progress"}**\n`;
});
return overview;
}
// Generate the main documentation content
function generateStepsRst (orderedSteps, scenarios) {
const flowOverview = generateFlowOverview(orderedSteps);
// Generate storage scenarios section
let scenariosSection = `
Storage Partitioning Scenarios
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Web UI installer provides several storage partitioning scenarios to accommodate different installation needs:
.. figure:: images/storage-scenario-overview.png
:width: 500px
:alt: Storage scenarios overview interface
`;
scenarios.forEach((scenario, index) => {
scenariosSection += `**${scenario.getLabel() || scenario.docsLabel}**\n\n`;
if (scenario.description) {
scenariosSection += `${scenario.description}\n`;
}
// Check if individual scenario image exists and include it
const scenarioImagePath = path.join(__dirname, "images", `storage-scenario-${scenario.id}.png`);
if (fs.existsSync(scenarioImagePath)) {
scenariosSection += `
.. figure:: images/storage-scenario-${scenario.id}.png
:width: 500px
:alt: ${scenario.getLabel() || scenario.docsLabel} scenario interface
`;
}
// Add spacing between scenarios except for the last one
if (index < scenarios.length - 1) {
scenariosSection += "\n";
}
});
const rstContent = `
Installation Flow Overview
==========================
This document describes the step-by-step process for installing the system using the Web UI.
${flowOverview}
Detailed Step Descriptions
=============================
${orderedSteps.map((step, index) => {
const stepNumber = index + 1;
const stepTitle = `${stepNumber}. ${!step.isFinal ? step.label : "Installation progress"}`;
let stepContent = stepTitle + "\n" + "-".repeat(stepTitle.length) + `\n\n${step.description || "No description available."}\n`;
// Add scenarios subsection for the Installation method step
if (step.id === "anaconda-screen-method") {
stepContent += `\n${scenariosSection}`;
}
return stepContent;
}).join("\n")}
`;
return rstContent;
}
// Main function to generate documentation
async function generateDocs () {
const mockingSystem = new MockingSystem();
try {
log("Starting documentation generation...");
log("Initializing mocking system...");
// Initialize the global mocking system
await mockingSystem.initialize();
log("Getting ordered steps...");
// Get ordered steps from getSteps()
const orderedSteps = await getOrderedSteps(mockingSystem);
log("Processing scenarios and pages...");
// Get all scenarios and pages
const scenariosDir = path.join(__dirname, "..", "src", "components", "storage", "scenarios");
const scenarioIndexFiles = findIndexFiles(scenariosDir);
// Filter out the root scenarios/index.js file - only include files in subdirectories
const scenarioFiles = scenarioIndexFiles.filter(filePath => {
const relativePath = path.relative(scenariosDir, filePath);
return relativePath !== "index.js"; // Exclude the root index.js
});
// Mock scenario files before trying to import them
await mockingSystem.mockScenarioFiles(scenarioFiles);
const scenarioPromises = scenarioFiles.map(filePath => extractScenarioInfo(filePath, mockingSystem));
const scenarios = await Promise.all(scenarioPromises);
// Find page files using findIndexFiles and filter out scenarios directory
const componentsDir = path.resolve(__dirname, "../src/components");
const allIndexFiles = findIndexFiles(componentsDir);
const pageFiles = allIndexFiles
.filter(filePath => !filePath.startsWith(scenariosDir));
// Mock page files before trying to import them
await mockingSystem.mockScenarioFiles(pageFiles);
const pagePromises = pageFiles.map(filePath => extractPageInfo(filePath, mockingSystem));
const pages = await Promise.all(pagePromises);
// Match pages with steps by ID
const stepsWithPageData = orderedSteps.map(step => {
const matchingPage = pages.find(page => page?.id === step.id);
return {
...step,
description: matchingPage?.description,
};
});
log("Generating documentation...");
// Generate the documentation
const stepsRst = generateStepsRst(stepsWithPageData, scenarios);
// Write the documentation
const outputPath = path.join(__dirname, "installation-steps.rst");
fs.writeFileSync(outputPath, stepsRst);
log("Documentation generation completed!");
} finally {
// Always cleanup temp files
mockingSystem.cleanup();
}
}
// Run the documentation generation
generateDocs();

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -0,0 +1,101 @@
Installation Flow Overview
==========================
This document describes the step-by-step process for installing the system using the Web UI.
1. **Welcome**
2. **Network**
3. **Date and time**
4. **Software selection**
5. **Installation method**
6. **Storage configuration**
7. **Create Account**
8. **Review and install**
9. **Installation progress**
Detailed Step Descriptions
=============================
1. Welcome
----------
Select the language & keyboard to use during installation and for the target system.
2. Network
----------
Configure network connections for the system.
3. Date and time
----------------
Configure your system's timezone, date, and time settings. You can also set up network time synchronization.
4. Software selection
---------------------
Select packages to install by choosing a base environment.
5. Installation method
----------------------
Choose the target device(s) for the installation and the partitioning scenario.
Storage Partitioning Scenarios
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Web UI installer provides several storage partitioning scenarios to accommodate different installation needs:
.. figure:: images/storage-scenario-overview.png
:width: 500px
:alt: Storage scenarios overview interface
**Use entire disk**
Completely erases all data on the selected disks and automatically creates a new partition layout. Use this for clean installations when you do not need to preserve any existing data.
**Mount point assignment**
Allows you to manually assign mount points to specific devices for complete control over the partition layout. Use this if you have custom storage requirements or want to reuse existing partitions selectively.
.. figure:: images/storage-scenario-mount-point-mapping.png
:width: 500px
:alt: Mount point assignment scenario interface
**Reinstall Fedora**
Reinstalls Fedora while preserving your existing home directory and user data. Use when you want to refresh your Fedora installation while keeping all your personal files and settings. This option only appears when exactly one existing Fedora system is detected and the system has only the default mount points.
**Use configured storage**
.. figure:: images/storage-scenario-use-configured-storage.png
:width: 500px
:alt: Use configured storage scenario interface
**Use free space**
Installs using only unallocated free space, preserving existing partitions and data. Use when you want to dual-boot with existing operating systems. This option only appears when existing partitions are detected on the selected disks.
6. Storage configuration
------------------------
Automatic partitioning configuration, disk encryption, and storage options.
7. Create Account
-----------------
Set up user accounts and administrator passwords for your system.
8. Review and install
---------------------
Review your installation settings and start the installation process.
9. Installation progress
------------------------
Monitor the installation progress and completion.

View file

@ -0,0 +1,5 @@
.. This page serves as an index for release notes.
This section contains release notes for Anaconda Web UI, organized by Fedora release.
.. include:: release-notes.rst

View file

@ -0,0 +1,52 @@
Release notes Web UI
====================
This document describes major Anaconda Web UI changes in Fedora releases.
Fedora 44
#########
Keyboard and localization
-------------------------
Multiple keyboard layouts on Workstation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Multiple keyboard layouts can be selected on Workstation, similar to non-GNOME
variants. This is needed for many languages (for example, Russian with the
Cyrillic alphabet).
Sensible keyboard defaults with language selection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When you choose a language, the installer suggests matching keyboard defaults
on non-GNOME systems. On Workstation ISO, keyboard layout is not set
automatically according to language selection; use GNOME’s keyboard settings to
add or switch layouts if needed.
See also:
- https://bugzilla.redhat.com/show_bug.cgi?id=2402459
Storage
-------
Validation warnings for automatic partitioning
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When automatic partitioning is selected, the Web UI can show validation warnings
when the chosen setup may need attention. These are warnings, not errors: the
user can ignore them and continue if they want. For example, when choosing to
share the disk with another OS, if the existing EFI partition is smaller than
the recommended size of 1 GiB, a warning is shown so the user is aware before
proceeding.
Other
-----
Improvements in error reporting
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The error reporting dialog now supports a flow that uses a Bugzilla API key to
register and automatically upload the needed logs. Stack traces are
automatically included in reports with readable source-mapped output. Users can
also restrict the report to a limited group.

View file

@ -0,0 +1,12 @@
:Type: Localization / Storage / Network / ...
:Summary: Summarize the change
:Description:
Describe the change in detail. The text
can be as long as you need.
Use empty lines to separate paragraphs.
:Links:
- Link to the bug on Bugzilla.
- Link to the pull request on GitHub.

View file

@ -0,0 +1,94 @@
=================
Release Process
=================
We trigger releases manually, not on a fixed schedule, and rely on GitHub Actions and Packit to handle the rest automatically.
This document outlines the detailed release procedure.
1. **Review release blockers**
Issues and PRs that are urgent for the next release should have the
`release-blocker <https://github.com/rhinstaller/anaconda-webui/labels/release-blocker>`_ label.
Review the list and confirm with the team whether any pending items remain for the current release.
2. **Identify the previous release**
Run::
PREV=$(git describe --abbrev=0 --tags); echo $PREV
3. **Review recent changes**
Generate a changelog with::
git shortlog --no-merges $PREV..
Focus on new features. Skip test-only changes and minor bug fixes.
4. **Prepare the release summary and tag**
Use the changelog to create a "release news" summary. This appears in:
- the GitHub release page
- downstream package changelogs (via the tag message)
Follow this tagging format::
123
- Add this cool new feature
- Support Python 3
- Fix wrong color on the bikeshed (rhbz#123456)
Create the tag::
git tag -s <new-tag>
Or use ``-a`` if you don't have a GPG key.
Push the tag to the main branch::
git push origin <new-tag>
5. **GitHub release automation**
Tagging triggers the `release GitHub workflow <https://github.com/rhinstaller/anaconda-webui/blob/main/.github/workflows/release.yml>`_,
which creates the upstream GitHub release.
To monitor the release progress and view logs:
- Visit the `Releases page <https://github.com/rhinstaller/anaconda-webui/releases>`_
- Click the tagged commit (7-character hash)
- Click the *Statuses* icon (✅, ❌, or 🟡) next to the commit subject
- Select a "propose-downstream" or "rpm-build" **Details** link to access the full run logs
Example logs page: ``https://github.com/rhinstaller/anaconda-webui/runs/...``
6. **Packit downstream packaging**
Packit will automatically open `Fedora dist-git pull requests <https://src.fedoraproject.org/rpms/anaconda-webui/pull-requests>`_
for Rawhide and supported Fedora releases.
Wait for the PRs to appear and pass:
- Scratch build checks
- Integration testing
Merge the PRs once they're ready.
Merging triggers:
- Koji builds
- Bodhi updates
Skipping the Koji Build
=======================
If you need to do a combined Bodhi update with another package (e.g. ``anaconda``) and want to
handle the Koji build and Bodhi update manually, add the ``skip-build`` label to the dist-git pull
request **before merging**. This prevents Packit from triggering the automatic Koji build.
You will then need to perform the Koji build and Bodhi update manually.

View file

@ -0,0 +1,51 @@
Fix ISO image refreshes for the WebUI tests
-------------------------------------------
In order to test the WebUI code we use the ISO images specified in `Cockpit's bots repository <https://github.com/cockpit-project/bots/tree/main/images>`_.
These ISO files are direct downloads from the official distribution server and their
`creation process is defined in bots repository <https://github.com/cockpit-project/bots/blob/main/images/scripts/fedora-rawhide-boot.bootstrap>`_.
The purpose of using these ISOs for the tests instead of downloading from the official server is to gate the image updates.
The default refresh period of the test images is one week, but they can also be `refreshed manually <https://github.com/cockpit-project/bots#refreshing-a-test-image>`_.
Image refreshes happen with `Pull Requests which update the current image SHA <https://github.com/cockpit-project/bots/pull/2981>`_.
The tests defined in `tests/` will run on the Pull request gating the `relevant to anaconda image refreshes <https://github.com/cockpit-project/bots/blob/main/lib/testmap.py>`_.
Image refreshes with successfull CI will be merged automagically from Cockpit team.
In the case an updated dependency makes the WebUI tests on the image refresh fail, the on-duty team
member is in charge of debugging the failure.
For this take the following steps:
* Locally checkout the bots repository branch used in the failing refresh PR. The path to the local bots checkout should be the `bots`.
* Create the test VM with the new image and debug by following the `WebUI test instructions <https://github.com/rhinstaller/anaconda-webui/tree/main/test#readme>`_
When the reason for the breackage is identified there are two options to go forward:
* If the failure comes from an intended change in behaviour adjust Anaconda or the tests
* If the failure uncovers an actual regression, file bugs for the given Fedora components. If it does not look like the issue will be
fixed quickly, work around in Anaconda or add a `naughty override file <https://github.com/cockpit-project/bots/tree/main/naughty/>`_, thus marking the expected failure pattern.
Fix a class of NPM errors that can break Web UI tests
-----------------------------------------------------
This issue manifests as failed build of Anaconda RPMs due to NPM errors, that prevents Web UI tests to be started at all. This differs
from an actual bug in NPM dependency specification or NPM package maintainers going all "it works on my machine" again in two key aspects:
* the same NPM error suddenly breaks Web UI tests on *all* PRs
* when Web UI tests are run locally outside of Cockpit CI the build succeeds and the test are started
What you are seeing is breakage in NPM caching the Cockpit CI is using to avoid issues with the massive NPM consumption of the infra.
The mechanism it usually works fine, but sometimes the cache update mechanism can get stuck, resulting in the cache going stale causing
this issue to manifest.
How to fix:
* retry the test run on the PR - sometimes no all the builders are currently affected, retrying might run the test on different builder
* report the issue to the Cockpit team - on #cockpit on Libera Chat IRC or as a new issue in https://github.com/cockpit-project/cockpit/issues
In the end, someone from the Cockpit team will tell the builders in the Cockpit infra to drop their cache, fixing the issue or the affected
builder possibly gets cleaned up by some automated mechanism over time.

View file

@ -0,0 +1,67 @@
[Desktop Entry]
Version=1.0
Name=Browser External
GenericName=Web Browser
GenericName[ca]=Navegador web
GenericName[cs]=Webový prohlížeč
GenericName[es]=Navegador web
GenericName[fa]=مرورگر اینترنتی
GenericName[fi]=WWW-selain
GenericName[fr]=Navigateur Web
GenericName[hu]=Webböngésző
GenericName[it]=Browser Web
GenericName[ja]=ウェブ・ブラウザ
GenericName[ko]=웹 브라우저
GenericName[nb]=Nettleser
GenericName[nl]=Webbrowser
GenericName[nn]=Nettlesar
GenericName[no]=Nettleser
GenericName[pl]=Przeglądarka WWW
GenericName[pt]=Navegador Web
GenericName[pt_BR]=Navegador Web
GenericName[sk]=Internetový prehliadač
GenericName[sv]=Webbläsare
Comment=Browse the Web
Comment[ca]=Navegueu per el web
Comment[cs]=Prohlížení stránek World Wide Webu
Comment[de]=Im Internet surfen
Comment[es]=Navegue por la web
Comment[fa]=صفحات شبکه جهانی اینترنت را مرور نمایید
Comment[fi]=Selaa Internetin WWW-sivuja
Comment[fr]=Navigue sur Internet
Comment[hu]=A világháló böngészése
Comment[it]=Esplora il web
Comment[ja]=ウェブを閲覧します
Comment[ko]=웹을 돌아 다닙니다
Comment[nb]=Surf på nettet
Comment[nl]=Verken het internet
Comment[nn]=Surf på nettet
Comment[no]=Surf på nettet
Comment[pl]=Przeglądanie stron WWW
Comment[pt]=Navegue na Internet
Comment[pt_BR]=Navegue na Internet
Comment[sk]=Prehliadanie internetu
Comment[sv]=Surfa på webben
TryExec=/usr/libexec/anaconda/browser-ext
Exec=/usr/libexec/anaconda/browser-ext %u
Terminal=false
Type=Application
MimeType=x-scheme-handler/extlink;
StartupNotify=true
Categories=Network;WebBrowser;
Actions=new-window;new-private-window;profile-manager-window;
NoDisplay=true
X-Desktop-File-Install-Version=0.26
[Desktop Action new-window]
Name=Open a New Window
Exec=firefox -new-window
[Desktop Action new-private-window]
Name=Open a New Private Window
Exec=firefox -private-window
[Desktop Action profile-manager-window]
Name=Open Profile Manager
Exec=firefox -ProfileManager

View file

@ -0,0 +1,12 @@
/* type selector "tab" is not standard CSS */
/* stylelint-disable selector-type-no-unknown */
/* Hide Firefox UI when there's just one tab */
#navigator-toolbox:has(tab:only-of-type) {
visibility: collapse;
}
/* Hide the close button */
.titlebar-close {
display: none;
}

View file

@ -0,0 +1,57 @@
/* global user_pref */
// Let us use userChrome.css
user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
// We want to use :has()
user_pref("layout.css.has-selector.enabled", true);
// Dev stuff (for easier UI adusting with ctrl+shift+alt+i)
user_pref("browser.aboutConfig.showWarning", false);
user_pref("devtools.chrome.enabled", true);
user_pref("devtools.debugger.remote-enabled", true);
user_pref("devtools.inspector.showUserAgentStyles", true);
// Don't have any startup page
user_pref("browser.startup.page", 0);
user_pref("browser.startup.homepage", "about:blank");
user_pref("browser.startup.homepage_override.once", {});
// Hide the bookmarks
user_pref("browser.toolbars.bookmarks.visibility", "never");
// Don't show anything special in the new tab page'
user_pref("browser.newtabpage.activity-stream.default.sites", "");
user_pref("browser.newtabpage.activity-stream.showSponsored", false);
user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
// Remove UI stuff
user_pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"unified-extensions-area\":[],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"urlbar-container\",\"downloads-button\",\"unified-extensions-button\"],\"toolbar-menubar\":[\"menubar-items\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"alltabs-button\"],\"PersonalToolbar\":[\"personal-bookmarks\"]},\"seen\":[\"save-to-pocket-button\",\"developer-button\"],\"dirtyAreaCache\":[\"nav-bar\",\"TabsToolbar\",\"toolbar-menubar\",\"PersonalToolbar\"],\"currentVersion\":19,\"newElementCount\":3}");
// Don't set a placeholder for the tab before the page loads and sets title
user_pref("browser.urlbar.placeholderName", "");
// Turn off calling home
user_pref("app.normandy.enabled", false);
user_pref("browser.discovery.enabled", false);
user_pref("datareporting.healthreport.uploadEnabled", false);
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("toolkit.telemetry.unified", false);
user_pref("trailhead.firstrun.didSeeAboutWelcome", true);
// Don't show `Allow this site to open the PROTOCOL link with APPLICATION` dialog
user_pref("network.protocol-handler.warn-external-default", false);
user_pref("network.protocol-handler.external.anaconda-gnome-control-center", true);
user_pref("network.protocol-handler.external.extlink", true);
// Don't show the `This site is trying to open a popup` dialog
user_pref("dom.disable_open_during_load", false);
// Disable password manager popups
user_pref("signon.showAutoCompleteFooter", false);
user_pref("signon.rememberSignons", false);
// Disable the translate popup
user_pref("browser.translations.enable", false);
// Don't auto-hide the toolbar in fullscreen
user_pref("browser.fullscreen.autohide", false);

View file

@ -0,0 +1,9 @@
/* global user_pref */
// Don't have any startup page
user_pref("browser.startup.page", 0);
user_pref("browser.startup.homepage", "about:blank");
user_pref("browser.startup.homepage_override.once", {});
// Disable password manager popups
user_pref("signon.showAutoCompleteFooter", false);
user_pref("signon.rememberSignons", false);

View file

@ -0,0 +1,12 @@
/* type selector "tab" is not standard CSS */
/* stylelint-disable selector-type-no-unknown */
/* Hide Firefox UI when there's just one tab */
#navigator-toolbox:has(tab:only-of-type) {
visibility: collapse;
}
/* Hide the close button */
.titlebar-close {
display: none;
}

View file

@ -0,0 +1,54 @@
/* global user_pref */
// Let us use userChrome.css
user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
// We want to use :has()
user_pref("layout.css.has-selector.enabled", true);
// Dev stuff (for easier UI adusting with ctrl+shift+alt+i)
user_pref("browser.aboutConfig.showWarning", false);
user_pref("devtools.chrome.enabled", true);
user_pref("devtools.debugger.remote-enabled", true);
user_pref("devtools.inspector.showUserAgentStyles", true);
// Don't have any startup page
user_pref("browser.startup.page", 0);
user_pref("browser.startup.homepage", "about:blank");
user_pref("browser.startup.homepage_override.once", "");
// Hide the bookmarks
user_pref("browser.toolbars.bookmarks.visibility", "never");
// Don't show anything special in the new tab page'
user_pref("browser.newtabpage.activity-stream.default.sites", "");
user_pref("browser.newtabpage.activity-stream.showSponsored", false);
user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
// Remove UI stuff
user_pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"unified-extensions-area\":[],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"urlbar-container\",\"downloads-button\",\"unified-extensions-button\"],\"toolbar-menubar\":[\"menubar-items\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"alltabs-button\"],\"PersonalToolbar\":[\"personal-bookmarks\"]},\"seen\":[\"save-to-pocket-button\",\"developer-button\"],\"dirtyAreaCache\":[\"nav-bar\",\"TabsToolbar\",\"toolbar-menubar\",\"PersonalToolbar\"],\"currentVersion\":19,\"newElementCount\":3}");
// Don't set a placeholder for the tab before the page loads and sets title
user_pref("browser.urlbar.placeholderName", "");
// Turn off calling home
user_pref("app.normandy.enabled", false);
user_pref("browser.discovery.enabled", false);
user_pref("datareporting.healthreport.uploadEnabled", false);
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("toolkit.telemetry.unified", false);
user_pref("trailhead.firstrun.didSeeAboutWelcome", true);
// Don't show `Allow this site to open the PROTOCOL link with APPLICATION` dialog
user_pref("network.protocol-handler.warn-external-default", false);
user_pref("network.protocol-handler.external.anaconda-gnome-control-center", true);
user_pref("network.protocol-handler.external.extlink", true);
// Don't show the `This site is trying to open a popup` dialog
user_pref("dom.disable_open_during_load", false);
// Disable password manager popups
user_pref("signon.showAutoCompleteFooter", false);
user_pref("signon.rememberSignons", false);
// Disable the translate popup
user_pref("browser.translations.enable", false);

View file

@ -0,0 +1,32 @@
#!/bin/bash
#
# Copyright (C) 2025 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Panel name argument is mandatory
if [[ -z "$1" ]]; then
echo "Error: Panel name argument is required" >&2
exit 1
fi
PANEL="$1"
# Use gapplication to activate existing instance (brings window to front if running)
# This handles single-instance app activation automatically
gapplication launch org.gnome.Settings 2>/dev/null
# Launch with panel argument
# Since gnome-control-center is single-instance, this will switch panels in existing window
exec /usr/bin/gnome-control-center "$PANEL"

View file

@ -0,0 +1,926 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="2257px" height="2257px" viewBox="0 0 2257 2257" enable-background="new 0 0 2257 2257" xml:space="preserve">
<rect x="0" y="0" width="2257" height="2257" fill="rgb(255,255,255)" /><g transform="translate(122,122)"><g transform="translate(488,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,0) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,61) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,122) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,183) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,183) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,183) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,183) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,183) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,244) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,244) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,244) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,244) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,244) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,244) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,244) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,305) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,366) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,427) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,488) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(244,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,549) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,610) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(244,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,671) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(244,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,732) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,793) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,854) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,915) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(244,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,976) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(244,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1037) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,1098) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1159) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,1220) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1281) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(244,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(305,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(427,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1342) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(61,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(183,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(244,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1403) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(366,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1464) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,1525) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1586) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1891,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1647) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1708) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(732,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1769) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(610,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(915,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(976,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1830) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(549,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(671,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(854,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1220,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1342,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1464,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1525,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1647,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1769,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1952,1891) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(488,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(793,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1037,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1098,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1159,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1281,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1403,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1586,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1830,1952) scale(0.61,0.61)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,0) scale(4.27, 4.27)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(1586,0) scale(4.27, 4.27)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(0,1586) scale(4.27, 4.27)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(122,122) scale(1.83, 1.83)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1708,122) scale(1.83, 1.83)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(122,1708) scale(1.83, 1.83)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g></g></svg>

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -0,0 +1,55 @@
{
"name": "anaconda-webui",
"description": "Scaffolding for a cockpit module",
"type": "module",
"main": "index.js",
"author": "",
"license": "LGPL-2.1",
"scripts": {
"watch": "ESBUILD_WATCH=true ./build.js",
"build": "./build.js",
"eslint": "eslint --ext .js --ext .jsx src/",
"eslint:fix": "eslint --fix --ext .js --ext .jsx src/",
"stylelint": "stylelint 'src/**/*{.css,scss}'",
"stylelint:fix": "stylelint --fix 'src/**/*{.css,scss}'"
},
"devDependencies": {
"esbuild": "0.28.2",
"esbuild-plugin-copy": "2.1.1",
"esbuild-sass-plugin": "3.7.0",
"eslint": "8.57.0",
"eslint-config-standard": "17.1.0",
"eslint-config-standard-jsx": "11.0.0",
"eslint-config-standard-react": "13.0.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-sort-destructure-keys": "^2.0.0",
"gettext-parser": "8.0.0",
"glob": "13.0.1",
"htmlparser": "1.7.7",
"jed": "1.1.1",
"qunit": "2.25.0",
"sass": "1.98.0",
"stdio": "2.1.3",
"stylelint": "17.14.1",
"stylelint-config-recommended-scss": "17.0.1",
"stylelint-config-standard-scss": "17.0.0",
"stylelint-formatter-pretty": "4.0.1"
},
"dependencies": {
"@patternfly/patternfly": "6.6.1",
"@patternfly/react-core": "6.6.1",
"@patternfly/react-table": "6.6.1",
"dequal": "2.0.3",
"luxon": "^3.6.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"stacktrace-js": "2.0.2",
"throttle-debounce": "5.0.2"
}
}

View file

@ -0,0 +1,120 @@
Name: anaconda-webui
Version: %{VERSION}
Release: 1%{?dist}
Summary: Anaconda installer Web interface
License: LGPL-2.1-or-later AND MIT
URL: https://github.com/rhinstaller/%{name}
Source0: https://github.com/rhinstaller/%{name}/releases/download/%{version}/%{name}-%{version}.tar.xz
BuildArch: noarch
BuildRequires: libappstream-glib
BuildRequires: make
BuildRequires: gettext
# Needed for the unitdir macro
BuildRequires: systemd-rpm-macros
%global anacondacorever 0
%if 0%{?fedora} > 41
%global anacondacorever 42.5
%endif
%global webui_slitherer_spin_identities fedora-release-identity-budgie or fedora-release-identity-cinnamon or fedora-release-identity-kde-desktop or fedora-release-identity-kde-mobile or fedora-release-identity-lxde or fedora-release-identity-lxqt or fedora-release-identity-matecompiz or fedora-release-identity-soas or fedora-release-identity-sway or fedora-release-identity-xfce or fedora-release-identity-i3
%global cockpitver 275
%define _unitdir /usr/lib/systemd/system
Requires: cockpit-storaged
Requires: cockpit-networkmanager
Requires: cockpit-bridge >= %{cockpitver}
Requires: cockpit-ws >= %{cockpitver}
Requires: anaconda-core >= %{anacondacorever}
Requires: python3-bugzilla
# Firefox dependency needs to be specified there as cockpit web-view does not have a hard dependency on Firefox as
# it can often fall back to a diferent browser. This does not work in the limited installer
# environment, so we need to make sure Firefox is available. Exclude on RHEL, only Flatpak version will be there.
%if ! 0%{?rhel}
Requires: (slitherer if (%{webui_slitherer_spin_identities}) else firefox)
%endif
%if 0%{?fedora}
Requires: system-logos
%endif
BuildRequires: desktop-file-utils
%{NPM_PROVIDES}
%description
Anaconda installer Web interface
%prep
%setup -q -n %{name}
%build
# Nothing to build
%install
%make_install PREFIX=/usr
desktop-file-install --dir=%{buildroot}%{_datadir}/applications %{buildroot}%{_datadir}/applications/extlinks.desktop
desktop-file-install --dir=%{buildroot}%{_datadir}/applications %{buildroot}%{_datadir}/applications/anaconda-gnome-control-center.desktop
%check
exit 0
# We have some integration tests, but those require running a VM, so that would
# be an overkill for RPM check script.
%files
%dir %{_datadir}/cockpit/anaconda-webui
%doc README.rst
%license LICENSE dist/index.js.LEGAL.txt dist/anacondaLogin.js.LEGAL.txt
%{_datadir}/cockpit/anaconda-webui/login.html
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.js
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.js.map
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.js.LEGAL.txt
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.css
%{_datadir}/cockpit/anaconda-webui/anacondaLogin.css.map
%{_datadir}/cockpit/anaconda-webui/qr-code-feedback.svg
%{_datadir}/cockpit/anaconda-webui/index.js.LEGAL.txt
%{_datadir}/cockpit/anaconda-webui/index.html
%{_datadir}/cockpit/anaconda-webui/index.js.gz
%{_datadir}/cockpit/anaconda-webui/index.js.map
%{_datadir}/cockpit/anaconda-webui/index.css.gz
%{_datadir}/cockpit/anaconda-webui/index.css.map
%{_datadir}/cockpit/anaconda-webui/manifest.json
%{_datadir}/cockpit/anaconda-webui/po.*.js.gz
%{_datadir}/cockpit/anaconda-webui/VERSION.txt
%{_libexecdir}/anaconda/cockpit-coproc-wrapper.sh
%{_libexecdir}/anaconda/anaconda-cockpit-conf-merge
%dir /etc/anaconda/cockpit
%config(noreplace) /etc/anaconda/cockpit/cockpit.conf
%dir /etc/anaconda/cockpit/conf.d
%dir %{_datadir}/anaconda/cockpit
%dir %{_datadir}/anaconda/cockpit/conf.d
%{_datadir}/anaconda/cockpit/conf.d/50-remote-auth.conf
%{_libexecdir}/anaconda/cockpit-pin-auth
%dir %{_datadir}/anaconda/firefox-theme
%dir %{_datadir}/anaconda/firefox-theme/default
%dir %{_datadir}/anaconda/firefox-theme/default/chrome
%{_datadir}/anaconda/firefox-theme/default/user.js
%{_datadir}/anaconda/firefox-theme/default/chrome/userChrome.css
%dir %{_datadir}/anaconda/firefox-theme/live
%dir %{_datadir}/anaconda/firefox-theme/live/chrome
%{_datadir}/anaconda/firefox-theme/live/user.js
%{_datadir}/anaconda/firefox-theme/live/chrome/userChrome.css
%dir %{_datadir}/anaconda/firefox-theme/extlink
%{_datadir}/anaconda/firefox-theme/extlink/user.js
%{_libexecdir}/anaconda/webui-desktop
%{_libexecdir}/anaconda/browser-ext
%{_libexecdir}/anaconda/gnome-control-center-ext
%{_datadir}/applications/extlinks.desktop
%{_datadir}/applications/anaconda-gnome-control-center.desktop
%{_unitdir}/webui-cockpit-ws.service
%{_unitdir}/cockpit-pin-auth.socket
%{_unitdir}/cockpit-pin-auth@.service
%{_unitdir}/webui-tls-proxy.service
%{_libexecdir}/anaconda/webui-tls-proxy
# The changelog is automatically generated and merged
%changelog

View file

@ -0,0 +1,53 @@
upstream_project_url: https://github.com/rhinstaller/anaconda-webui
# enable notification of failed downstream jobs as issues
issue_repository: https://github.com/rhinstaller/anaconda-webui
specfile_path: anaconda-webui.spec
upstream_package_name: anaconda-webui
downstream_package_name: anaconda-webui
# use the nicely formatted release description from our upstream release, instead of git shortlog
copy_upstream_release_description: true
srpm_build_deps:
- npm
- make
actions:
post-upstream-clone: make anaconda-webui.spec
create-archive: make dist
jobs:
- job: tests
trigger: pull_request
targets:
- fedora-development
- job: copr_build
trigger: pull_request
targets:
- fedora-development
# for cross-project testing
- job: copr_build
trigger: commit
owner: "@rhinstaller"
project: "Anaconda-webui"
preserve_project: True
- job: propose_downstream
trigger: release
dist_git_branches:
- fedora-development
- job: koji_build
trigger: commit
require:
label:
absent:
- skip-build
dist_git_branches:
- fedora-development
- job: bodhi_update
trigger: koji_build
dist_git_branches:
- fedora-development

View file

6432
installer/anaconda-webui/vendor/po/af.po vendored Normal file

File diff suppressed because it is too large Load diff

1867
installer/anaconda-webui/vendor/po/am.po vendored Normal file

File diff suppressed because it is too large Load diff

5439
installer/anaconda-webui/vendor/po/ar.po vendored Normal file

File diff suppressed because it is too large Load diff

6333
installer/anaconda-webui/vendor/po/as.po vendored Normal file

File diff suppressed because it is too large Load diff

3894
installer/anaconda-webui/vendor/po/ast.po vendored Normal file

File diff suppressed because it is too large Load diff

8336
installer/anaconda-webui/vendor/po/be.po vendored Normal file

File diff suppressed because it is too large Load diff

7477
installer/anaconda-webui/vendor/po/bg.po vendored Normal file

File diff suppressed because it is too large Load diff

3053
installer/anaconda-webui/vendor/po/bn.po vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

8307
installer/anaconda-webui/vendor/po/ca.po vendored Normal file

File diff suppressed because it is too large Load diff

8608
installer/anaconda-webui/vendor/po/cs.po vendored Normal file

File diff suppressed because it is too large Load diff

1901
installer/anaconda-webui/vendor/po/cy.po vendored Normal file

File diff suppressed because it is too large Load diff

8478
installer/anaconda-webui/vendor/po/da.po vendored Normal file

File diff suppressed because it is too large Load diff

8725
installer/anaconda-webui/vendor/po/de.po vendored Normal file

File diff suppressed because it is too large Load diff

2935
installer/anaconda-webui/vendor/po/el.po vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

7469
installer/anaconda-webui/vendor/po/eo.po vendored Normal file

File diff suppressed because it is too large Load diff

8702
installer/anaconda-webui/vendor/po/es.po vendored Normal file

File diff suppressed because it is too large Load diff

2735
installer/anaconda-webui/vendor/po/et.po vendored Normal file

File diff suppressed because it is too large Load diff

3788
installer/anaconda-webui/vendor/po/eu.po vendored Normal file

File diff suppressed because it is too large Load diff

2880
installer/anaconda-webui/vendor/po/fa.po vendored Normal file

File diff suppressed because it is too large Load diff

8638
installer/anaconda-webui/vendor/po/fi.po vendored Normal file

File diff suppressed because it is too large Load diff

2505
installer/anaconda-webui/vendor/po/fil.po vendored Normal file

File diff suppressed because it is too large Load diff

8909
installer/anaconda-webui/vendor/po/fr.po vendored Normal file

File diff suppressed because it is too large Load diff

8750
installer/anaconda-webui/vendor/po/fur.po vendored Normal file

File diff suppressed because it is too large Load diff

3943
installer/anaconda-webui/vendor/po/fy.po vendored Normal file

File diff suppressed because it is too large Load diff

3118
installer/anaconda-webui/vendor/po/ga.po vendored Normal file

File diff suppressed because it is too large Load diff

3472
installer/anaconda-webui/vendor/po/gl.po vendored Normal file

File diff suppressed because it is too large Load diff

5741
installer/anaconda-webui/vendor/po/gu.po vendored Normal file

File diff suppressed because it is too large Load diff

8385
installer/anaconda-webui/vendor/po/he.po vendored Normal file

File diff suppressed because it is too large Load diff

4191
installer/anaconda-webui/vendor/po/hi.po vendored Normal file

File diff suppressed because it is too large Load diff

8510
installer/anaconda-webui/vendor/po/hr.po vendored Normal file

File diff suppressed because it is too large Load diff

8647
installer/anaconda-webui/vendor/po/hu.po vendored Normal file

File diff suppressed because it is too large Load diff

5022
installer/anaconda-webui/vendor/po/ia.po vendored Normal file

File diff suppressed because it is too large Load diff

8675
installer/anaconda-webui/vendor/po/id.po vendored Normal file

File diff suppressed because it is too large Load diff

2100
installer/anaconda-webui/vendor/po/is.po vendored Normal file

File diff suppressed because it is too large Load diff

8842
installer/anaconda-webui/vendor/po/it.po vendored Normal file

File diff suppressed because it is too large Load diff

8660
installer/anaconda-webui/vendor/po/ja.po vendored Normal file

File diff suppressed because it is too large Load diff

8043
installer/anaconda-webui/vendor/po/ka.po vendored Normal file

File diff suppressed because it is too large Load diff

1871
installer/anaconda-webui/vendor/po/kab.po vendored Normal file

File diff suppressed because it is too large Load diff

8496
installer/anaconda-webui/vendor/po/kk.po vendored Normal file

File diff suppressed because it is too large Load diff

3600
installer/anaconda-webui/vendor/po/km.po vendored Normal file

File diff suppressed because it is too large Load diff

5875
installer/anaconda-webui/vendor/po/kn.po vendored Normal file

File diff suppressed because it is too large Load diff

8521
installer/anaconda-webui/vendor/po/ko.po vendored Normal file

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more