diff --git a/.babelrc b/.babelrc deleted file mode 100644 index d0ef093..0000000 --- a/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": ["@babel/env", - "@babel/preset-react"] -} diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 0000000..8d81802 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,22 @@ +container: + # official cockpit CI container, with cockpit related build and test dependencies + # if you want to use your own, see the documentation about required packages: + # https://github.com/cockpit-project/cockpit/blob/main/HACKING.md#getting-the-development-dependencies + image: ghcr.io/cockpit-project/tasks + kvm: true + # increase this if you have many tests that benefit from parallelism + cpu: 1 + +test_task: + env: + matrix: + - TEST_OS: fedora-42 + - TEST_OS: centos-9-stream + + fix_kvm_script: sudo chmod 666 /dev/kvm + + # test PO template generation + pot_build_script: make po/starter-kit.pot + + # chromium has too little /dev/shm, and we can't make that bigger + check_script: TEST_BROWSER=firefox TEST_JOBS=$(nproc) TEST_OS=$TEST_OS make check diff --git a/.cockpit-ci/container b/.cockpit-ci/container new file mode 100644 index 0000000..c6a7065 --- /dev/null +++ b/.cockpit-ci/container @@ -0,0 +1 @@ +ghcr.io/cockpit-project/tasks:2025-07-26 diff --git a/.eslintignore b/.eslintignore index 8d87b1d..85f5a45 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ node_modules/* +pkg/lib/* diff --git a/.eslintrc.json b/.eslintrc.json index 3b7538e..d499b3e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,18 +1,15 @@ { + "root": true, "env": { "browser": true, "es6": true }, - "extends": ["eslint:recommended", "standard", "standard-react"], - "parser": "babel-eslint", + "extends": ["eslint:recommended", "standard", "standard-jsx", "standard-react"], "parserOptions": { - "ecmaFeatures": { - "experimentalObjectRestSpread": true, - "jsx": true - }, + "ecmaVersion": "2022", "sourceType": "module" }, - "plugins": ["flowtype", "react"], + "plugins": ["react", "react-hooks"], "rules": { "indent": ["error", 4, { @@ -22,11 +19,15 @@ "ignoredNodes": [ "JSXAttribute" ] }], "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }], + "no-var": "error", "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], "prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }], "react/jsx-indent": ["error", 4], "semi": ["error", "always", { "omitLastInOneLineBlock": true }], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "camelcase": "off", "comma-dangle": "off", "curly": "off", @@ -36,17 +37,25 @@ "quotes": "off", "react/jsx-curly-spacing": "off", "react/jsx-indent-props": "off", + "react/jsx-no-useless-fragment": "error", "react/prop-types": "off", "space-before-function-paren": "off", - "standard/no-callback-literal": "off", - - "eqeqeq": "off", - "import/no-webpack-loader-syntax": "off", - "object-property-newline": "off", - "react/jsx-no-bind": "off" + "standard/no-callback-literal": "off" }, "globals": { - "require": false, - "module": false - } + "require": "readonly", + "module": "readonly" + }, + "overrides": [ + { + "files": ["**/*.ts", "**/*.tsx"], + "plugins": [ + "@typescript-eslint" + ], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": ["./tsconfig.json"] + } + }] } diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..cf4c387 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 118 diff --git a/.fmf/version b/.fmf/version new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/.fmf/version @@ -0,0 +1 @@ +1 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ccce5b3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,51 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + # run these when most of our developers don't work, don't DoS our CI over the day + time: "22:00" + timezone: "Europe/Berlin" + open-pull-requests-limit: 3 + groups: + eslint: + patterns: + - "eslint*" + esbuild: + patterns: + - "esbuild*" + patternfly: + patterns: + - "@patternfly*" + react: + patterns: + - "react*" + stylelint: + patterns: + - "stylelint*" + types: + patterns: + - "@types*" + - "types*" + + ignore: + # https://github.com/cockpit-project/cockpit/issues/21151 + - dependency-name: "sass" + versions: [">=1.80.0", "2.x"] + + # needs to be done in Cockpit first + - dependency-name: "@patternfly/*" + update-types: ["version-update:semver-major"] + + # PF5 requires React 18 + - dependency-name: "*react*" + update-types: ["version-update:semver-major"] + + - package-ecosystem: "github-actions" + directory: "/" + open-pull-requests-limit: 3 + labels: + - "no-test" + schedule: + interval: "weekly" diff --git a/.github/workflows/cockpit-lib-update.yml b/.github/workflows/cockpit-lib-update.yml new file mode 100644 index 0000000..d3dfa70 --- /dev/null +++ b/.github/workflows/cockpit-lib-update.yml @@ -0,0 +1,30 @@ +name: cockpit-lib-update +on: + schedule: + - cron: '0 2 * * 4' + # can be run manually on https://github.com/cockpit-project/starter-kit/actions + workflow_dispatch: +jobs: + cockpit-lib-update: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: 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@v4 + + - name: Run cockpit-lib-update + run: | + make bots + bots/cockpit-lib-update diff --git a/.github/workflows/release.yml.disabled b/.github/workflows/release.yml.disabled new file mode 100644 index 0000000..161b184 --- /dev/null +++ b/.github/workflows/release.yml.disabled @@ -0,0 +1,38 @@ +# Create a GitHub upstream release. Replace "TARNAME" with your project tarball +# name and enable this by dropping the ".disabled" suffix from the file name. +# See README.md. +name: release +on: + push: + tags: + # this is a glob, not a regexp + - '[0-9]*' +jobs: + source: + runs-on: ubuntu-latest + container: + image: ghcr.io/cockpit-project/tasks:latest + options: --user root + permissions: + # create GitHub release + contents: write + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # https://github.blog/2022-04-12-git-security-vulnerability-announced/ + - name: Pacify git's permission check + run: git config --global --add safe.directory /__w/ + + - name: Workaround for https://github.com/actions/checkout/pull/697 + run: git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags) + + - name: Build release + run: make dist + + - name: Publish GitHub release + uses: cockpit-project/action-release@7d2e2657382e8d34f88a24b5987f2b81ea165785 + with: + filename: "TARNAME-${{ github.ref_name }}.tar.xz" diff --git a/.github/workflows/tasks-container-update.yml b/.github/workflows/tasks-container-update.yml new file mode 100644 index 0000000..f572569 --- /dev/null +++ b/.github/workflows/tasks-container-update.yml @@ -0,0 +1,34 @@ +name: tasks-container-update +on: + schedule: + - cron: '0 2 * * 1' + # can be run manually on https://github.com/cockpit-project/starter-kit/actions + workflow_dispatch: +jobs: + tasks-container-update: + 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 + + # https://github.blog/2022-04-12-git-security-vulnerability-announced/ + - name: Pacify git's permission check + run: git config --global --add safe.directory /__w/starter-kit/starter-kit + + - name: Run tasks-container-update + run: | + make bots + bots/tasks-container-update diff --git a/.gitignore b/.gitignore index 32b6401..a439cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,35 @@ -*~ -*.retry -*.tar.gz +# 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 -node_modules/ -dist/ -/*.spec -/.vagrant -package-lock.json -Test*FAIL* -bots/ -test/common/ -test/images/ -*.pot -POTFILES* + +# toplevel (/...) +/Test*.html +/Test*.json +/Test*.log +/Test*.log.gz +/Test*.png +/*.whl +/bots +/cockpit-*.tar.xz +/cockpit-navigator.spec +/dist/ +/package-lock.json +/pkg/ +/node_modules/ +/tmp/ +/tools/ + +# subdirs (/subdir/...) +/packaging/arch/PKGBUILD +/packaging/debian/changelog +/po/*.pot + /po/LINGUAS +/test/common/ +/test/images/ +/test/static-code diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..5ba11dc --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,34 @@ +{ + "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, + "no-descending-specificity": null, + "no-duplicate-selectors": null, + "scss/at-extend-no-missing-placeholder": null, + "scss/load-partial-extension": null, + "scss/at-import-no-partial-leading-underscore": null, + "scss/load-no-partial-leading-underscore": true, + "scss/at-mixin-pattern": null, + "scss/comment-no-empty": null, + "scss/dollar-variable-pattern": null, + "scss/double-slash-comment-whitespace-inside": null, + "scss/no-global-function-names": null, + "scss/operator-no-unspaced": null, + "selector-class-pattern": null, + "selector-id-pattern": null + } +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e5d53fc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -dist: trusty -sudo: false -language: node_js -node_js: - - "8" -script: - - npm install - - npm run build diff --git a/Makefile b/Makefile index 6bd672c..06baee1 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,45 @@ # extract name from package.json PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' package.json) +RPM_NAME := cockpit-$(PACKAGE_NAME) VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.') ifeq ($(TEST_OS),) -TEST_OS = centos-7 +TEST_OS = centos-9-stream endif export TEST_OS +TARFILE=$(RPM_NAME)-$(VERSION).tar.xz +NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz +SPEC=$(RPM_NAME).spec +PREFIX ?= /usr/local +APPSTREAMFILE=org.cockpit_project.$(subst -,_,$(PACKAGE_NAME)).metainfo.xml VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS) -# one example directory from `npm install` to check if that already ran -NODE_MODULES_TEST=node_modules/po2json -# one example file in dist/ from webpack to check if that already ran -WEBPACK_TEST=dist/index.html +# stamp file to check for node_modules/ +NODE_MODULES_TEST=package-lock.json +# one example file in dist/ from bundler to check if that already ran +DIST_TEST=dist/manifest.json +# one example file in pkg/lib to check if it was already checked out +COCKPIT_REPO_STAMP=pkg/lib/cockpit-po-plugin.js +# 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 -all: $(WEBPACK_TEST) +all: $(DIST_TEST) + +# 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 \ + $(NULL) + +COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git +COCKPIT_REPO_COMMIT = 8076a6044ea41f378547d04e9f539a77f63191dc # 343 + 1 commits + +$(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 # # i18n @@ -19,89 +47,104 @@ all: $(WEBPACK_TEST) LINGUAS=$(basename $(notdir $(wildcard po/*.po))) -po/POTFILES.js.in: - mkdir -p $(dir $@) - find src/ -name '*.js' -o -name '*.jsx' -o -name '*.es6' > $@ - -po/$(PACKAGE_NAME).js.pot: po/POTFILES.js.in - xgettext --default-domain=cockpit --output=$@ --language=C --keyword= \ - --keyword=_:1,1t --keyword=_:1c,2,1t --keyword=C_:1c,2 \ +po/$(PACKAGE_NAME).js.pot: + xgettext --default-domain=$(PACKAGE_NAME) --output=- --language=C --keyword= \ + --add-comments=Translators: \ + --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 --files-from=$^ + --from-code=UTF-8 $$(find src/ -name '*.[jt]s' -o -name '*.[jt]sx') | \ + sed '/^#/ s/, c-format//' > $@ -po/POTFILES.html.in: - mkdir -p $(dir $@) - find src -name '*.html' > $@ +po/$(PACKAGE_NAME).html.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) + pkg/lib/html2po -o $@ $$(find src -name '*.html') -po/$(PACKAGE_NAME).html.pot: po/POTFILES.html.in - po/html2po -f $^ -o $@ +po/$(PACKAGE_NAME).manifest.pot: $(COCKPIT_REPO_STAMP) + pkg/lib/manifest2po -o $@ src/manifest.json -po/$(PACKAGE_NAME).manifest.pot: - po/manifest2po src/manifest.json -o $@ +po/$(PACKAGE_NAME).metainfo.pot: $(APPSTREAMFILE) + xgettext --default-domain=$(PACKAGE_NAME) --output=$@ $< -po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot +po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot po/$(PACKAGE_NAME).metainfo.pot msgcat --sort-output --output-file=$@ $^ -# Update translations against current PO template -update-po: po/$(PACKAGE_NAME).pot - for lang in $(LINGUAS); do \ - msgmerge --output-file=po/$$lang.po po/$$lang.po $<; \ - done - -dist/po.%.js: po/%.po $(NODE_MODULES_TEST) - mkdir -p $(dir $@) - po/po2json -m po/po.empty.js -o $@.js.tmp $< - mv $@.js.tmp $@ +po/LINGUAS: + echo $(LINGUAS) | tr ' ' '\n' > $@ # # Build/Install/dist # -%.spec: %.spec.in - sed -e 's/@VERSION@/$(VERSION)/g' $< > $@ +$(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' $< > $@ -$(WEBPACK_TEST): $(NODE_MODULES_TEST) $(shell find src/ -type f) package.json webpack.config.js $(patsubst %,dist/po.%.js,$(LINGUAS)) - NODE_ENV=$(NODE_ENV) npm run build +packaging/arch/PKGBUILD: packaging/arch/PKGBUILD.in + sed 's/VERSION/$(VERSION)/; s/SOURCE/$(TARFILE)/' $< > $@ + +$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js + NODE_ENV=$(NODE_ENV) ./build.js + +watch: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) + NODE_ENV=$(NODE_ENV) ./build.js --watch clean: rm -rf dist/ - [ ! -e cockpit-$(PACKAGE_NAME).spec.in ] || rm -f cockpit-$(PACKAGE_NAME).spec + rm -f $(SPEC) packaging/arch/PKGBUILD + rm -f po/LINGUAS -install: $(WEBPACK_TEST) - mkdir -p $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME) - cp -r dist/* $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME) - mkdir -p $(DESTDIR)/usr/share/metainfo/ - cp org.cockpit-project.$(PACKAGE_NAME).metainfo.xml $(DESTDIR)/usr/share/metainfo/ +install: $(DIST_TEST) po/LINGUAS + mkdir -p $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME) + cp -r dist/* $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME) + mkdir -p $(DESTDIR)$(PREFIX)/share/metainfo/ + msgfmt --xml -d po \ + --template $(APPSTREAMFILE) \ + -o $(DESTDIR)$(PREFIX)/share/metainfo/$(APPSTREAMFILE) # this requires a built source tree and avoids having to install anything system-wide -devel-install: $(WEBPACK_TEST) +devel-install: $(DIST_TEST) mkdir -p ~/.local/share/cockpit ln -s `pwd`/dist ~/.local/share/cockpit/$(PACKAGE_NAME) -# when building a distribution tarball, call webpack with a 'production' environment -# ship a stub node_modules/ so that `make` works without re-running `npm install` -dist-gzip: NODE_ENV=production -dist-gzip: all cockpit-$(PACKAGE_NAME).spec - mv node_modules node_modules.release - mkdir -p $(NODE_MODULES_TEST) - touch -r package.json $(NODE_MODULES_TEST) - touch dist/* - tar czf cockpit-$(PACKAGE_NAME)-$(VERSION).tar.gz --transform 's,^,cockpit-$(PACKAGE_NAME)/,' \ - --exclude cockpit-$(PACKAGE_NAME).spec.in \ - $$(git ls-files) cockpit-$(PACKAGE_NAME).spec dist/ node_modules - rm -rf node_modules - mv node_modules.release node_modules +# assumes that there was symlink set up using the above devel-install target, +# and removes it +devel-uninstall: + rm -f ~/.local/share/cockpit/$(PACKAGE_NAME) -srpm: dist-gzip cockpit-$(PACKAGE_NAME).spec +print-version: + @echo "$(VERSION)" + +dist: $(TARFILE) + @ls -1 $(TARFILE) + +# when building a distribution tarball, call bundler with a 'production' environment +# we don't ship node_modules for license and compactness reasons; 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) packaging/arch/PKGBUILD + if type appstream-util >/dev/null 2>&1; then appstream-util validate-relax --nonet *.metainfo.xml; fi + tar --xz $(TAR_ARGS) -cf $(TARFILE) --transform 's,^,$(RPM_NAME)/,' \ + --exclude packaging/$(SPEC).in --exclude node_modules \ + $$(git ls-files) $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) \ + $(SPEC) packaging/arch/PKGBUILD dist/ + +$(NODE_CACHE): $(NODE_MODULES_TEST) + tar --xz $(TAR_ARGS) -cf $@ node_modules + +node-cache: $(NODE_CACHE) + +# convenience target for developers +srpm: $(TARFILE) $(NODE_CACHE) $(SPEC) rpmbuild -bs \ --define "_sourcedir `pwd`" \ --define "_srcrpmdir `pwd`" \ - cockpit-$(PACKAGE_NAME).spec + $(SPEC) -rpm: dist-gzip cockpit-$(PACKAGE_NAME).spec +# convenience target for developers +rpm: $(TARFILE) $(NODE_CACHE) $(SPEC) mkdir -p "`pwd`/output" mkdir -p "`pwd`/rpmbuild" rpmbuild -bb \ @@ -111,39 +154,48 @@ rpm: dist-gzip cockpit-$(PACKAGE_NAME).spec --define "_srcrpmdir `pwd`" \ --define "_rpmdir `pwd`/output" \ --define "_buildrootdir `pwd`/build" \ - cockpit-$(PACKAGE_NAME).spec + $(SPEC) find `pwd`/output -name '*.rpm' -printf '%f\n' -exec mv {} . \; rm -r "`pwd`/rpmbuild" rm -r "`pwd`/output" "`pwd`/build" -# build a VM with locally built rpm installed -$(VM_IMAGE): rpm bots - rm -f $(VM_IMAGE) $(VM_IMAGE).qcow2 - bots/image-customize -v -i cockpit -i `pwd`/cockpit-$(PACKAGE_NAME)-*.noarch.rpm -s $(CURDIR)/test/vm.install $(TEST_OS) +# build a VM with locally built distro pkgs installed +# disable networking, VM images have mock/pbuilder with the common build dependencies pre-installed +$(VM_IMAGE): export XZ_OPT=-0 +$(VM_IMAGE): $(TARFILE) $(NODE_CACHE) bots test/vm.install + bots/image-customize --no-network --fresh \ + --upload $(NODE_CACHE):/var/tmp/ --build $(TARFILE) \ + --script $(CURDIR)/test/vm.install $(TEST_OS) # convenience target for the above vm: $(VM_IMAGE) - echo $(VM_IMAGE) + @echo $(VM_IMAGE) -# run the browser integration tests; skip check for SELinux denials -check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common - TEST_AUDIT_NO_SELINUX=1 test/check-application +# convenience target to print the filename of the test image +print-vm: + @echo $(VM_IMAGE) -# checkout Cockpit's bots/ directory for standard test VM images and API to launch them -# must be from cockpit's master, as only that has current and existing images; but testvm.py API is stable -bots: - git fetch --depth=1 https://github.com/cockpit-project/cockpit.git - git checkout --force FETCH_HEAD -- bots/ - git reset bots +# convenience target to setup all the bits needed for the integration tests +# without actually running them +prepare-check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common -# checkout Cockpit's test API; this has no API stability guarantee, so check out a stable tag -# when you start a new project, use the latest relese, and update it from time to time -test/common: - git fetch --depth=1 https://github.com/cockpit-project/cockpit.git 176 - git checkout --force FETCH_HEAD -- test/common - git reset test/common +# run the browser integration tests +# this will run all tests/check-* and format them as TAP +check: prepare-check + test/common/run-tests ${RUN_TESTS_OPTIONS} + +codecheck: test/common $(NODE_MODULES_TEST) + test/common/static-code + +# checkout Cockpit's bots for standard test VM images and API to launch them +bots: $(COCKPIT_REPO_STAMP) + test/common/make-bots $(NODE_MODULES_TEST): package.json - npm install + # if it exists already, npm install won't update it; force that so that we always get up-to-date packages + rm -f package-lock.json + # unset NODE_ENV, skips devDependencies otherwise + env -u NODE_ENV npm install --ignore-scripts + env -u NODE_ENV npm prune -.PHONY: all clean install devel-install dist-gzip srpm rpm check vm update-po +.PHONY: all clean install devel-install devel-uninstall print-version dist node-cache rpm prepare-check check vm print-vm diff --git a/README.md b/README.md index 1f408d0..284459b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ # Cockpit Starter Kit -Scaffolding for a [Cockpit](http://www.cockpit-project.org) module. +Scaffolding for a [Cockpit](https://cockpit-project.org/) module. + +# Development dependencies + +On Debian/Ubuntu: + + sudo apt install gettext nodejs npm make + +On Fedora: + + sudo dnf install gettext nodejs npm make + # Getting and building the source -Make sure you have `npm` available (usually from your distribution package). These commands check out the source and build it into the `dist/` directory: ``` @@ -15,15 +25,17 @@ make # Installing -`make install` compiles and installs the package in `/usr/share/cockpit/`. The +`make install` compiles and installs the package in `/usr/local/share/cockpit/`. The convenience targets `srpm` and `rpm` build the source and binary rpms, -respectively. Both of these make use of the `dist-gzip` target, which is used +respectively. Both of these make use of the `dist` target, which is used to generate the distribution tarball. In `production` mode, source files are automatically minified and compressed. Set `NODE_ENV=production` if you want to duplicate this behavior. For development, you usually want to run your module straight out of the git -tree. To do that, link that to the location were `cockpit-bridge` looks for packages: +tree. To do that, run `make devel-install`, which links your checkout to the +location were cockpit-bridge looks for packages. If you prefer to do +this manually: ``` mkdir -p ~/.local/share/cockpit @@ -33,50 +45,113 @@ ln -s `pwd`/dist ~/.local/share/cockpit/starter-kit After changing the code and running `make` again, reload the Cockpit page in your browser. +You can also use +[watch mode](https://esbuild.github.io/api/#watch) to +automatically update the bundle on every code change with + + ./build.js -w + +or + + make watch + +When developing against a virtual machine, watch mode can also automatically upload +the code changes by setting the `RSYNC` environment variable to +the remote hostname. + + RSYNC=c make watch + +When developing against a remote host as a normal user, `RSYNC_DEVEL` can be +set to upload code changes to `~/.local/share/cockpit/` instead of +`/usr/local`. + + RSYNC_DEVEL=example.com make watch + +To "uninstall" the locally installed version, run `make devel-uninstall`, or +remove manually the symlink: + + rm ~/.local/share/cockpit/starter-kit + # Running eslint Cockpit Starter Kit uses [ESLint](https://eslint.org/) to automatically check -JavaScript code style in `.jsx` and `.es6` files. +JavaScript/TypeScript code style in `.js[x]` and `.ts[x]` files. -The linter is executed within every build as a webpack preloader. +eslint is executed as part of `test/static-code`, aka. `make codecheck`. For developer convenience, the ESLint can be started explicitly by: - $ npm run eslint + npm run eslint Violations of some rules can be fixed automatically by: - $ npm run eslint:fix + npm run eslint:fix Rules configuration can be found in the `.eslintrc.json` file. -# Automated Testing +## 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/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. + +# Running tests locally Run `make check` to build an RPM, install it into a standard Cockpit test VM -(centos-7 by default), and run the test/check-application integration test on +(centos-9-stream by default), and run the test/check-application integration test on it. This uses Cockpit's Chrome DevTools Protocol based browser tests, through a Python API abstraction. Note that this API is not guaranteed to be stable, so if you run into failures and don't want to adjust tests, consider checking out -Cockpit's test/common from a tag instead of master (see the `test/common` +Cockpit's test/common from a tag instead of main (see the `test/common` target in `Makefile`). After the test VM is prepared, you can manually run the test without rebuilding the VM, possibly with extra options for tracing and halting on test failures (for interactive debugging): - TEST_OS=centos-7 test/check-application -tvs + TEST_OS=centos-9-stream test/check-application -tvs + +It is possible to setup the test environment without running the tests: + + TEST_OS=centos-9-stream make prepare-check You can also run the test against a different Cockpit image, for example: - TEST_OS=fedora-28 make check + TEST_OS=fedora-40 make check -# Vagrant +# Running tests in CI -This directory contains a Vagrantfile that installs and starts cockpit on a -Fedora 26 cloud image. Run `vagrant up` to start it and `vagrant rsync` to -synchronize the `dist` directory to `/usr/local/share/cockit/starter-kit`. Use -`vagrant rsync-auto` to automatically sync when contents of the `dist` -directory change. +These tests can be run in [Cirrus CI](https://cirrus-ci.org/), on their free +[Linux Containers](https://cirrus-ci.org/guide/linux/) environment which +explicitly supports `/dev/kvm`. Please see [Quick +Start](https://cirrus-ci.org/guide/quick-start/) how to set up Cirrus CI for +your project after forking from starter-kit. + +The included [.cirrus.yml](./.cirrus.yml) runs the integration tests for two +operating systems (Fedora and CentOS 8). Note that if/once your project grows +bigger, or gets frequent changes, you may need to move to a paid account, or +different infrastructure with more capacity. + +Tests also run in [Packit](https://packit.dev/) for all currently supported +Fedora releases; see the [packit.yaml](./packit.yaml) control file. You need to +[enable Packit-as-a-service](https://packit.dev/docs/packit-service/) in your GitHub project to use this. +To run the tests in the exact same way for upstream pull requests and for +[Fedora package update gating](https://docs.fedoraproject.org/en-US/ci/), the +tests are wrapped in the [FMF metadata format](https://github.com/teemtee/fmf) +for using with the [tmt test management tool](https://docs.fedoraproject.org/en-US/ci/tmt/). +Note that Packit tests can *not* run their own virtual machine images, thus +they only run [@nondestructive tests](https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py). # Customizing @@ -90,20 +165,36 @@ change: # Automated release Once your cloned project is ready for a release, you should consider automating -that. [Cockpituous release](https://github.com/cockpit-project/cockpituous/tree/master/release) -aims to fully automate project releases to GitHub, Fedora, Ubuntu, COPR, Docker -Hub, and other places. The intention is that the only manual step for releasing -a project is to create a signed tag for the version number; pushing the tag -then triggers a GitHub webhook that calls a set of release scripts (on -Cockpit's CI infrastructure). +that. 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: -starter-kit includes an example [cockpitous release script](./cockpituous-release) -that builds an upstream release tarball and source RPM. Please see the above -cockpituous documentation for details. +``` +123 + +- this new feature +- fix bug #123 +``` + +Pushing the release tag triggers the [release.yml](.github/workflows/release.yml.disabled) +[GitHub action](https://github.com/features/actions) workflow. This creates the +official release tarball and publishes as upstream release to GitHub. The +workflow is disabled by default -- to use it, edit the file as per the comment +at the top, and rename it to just `*.yml`. + +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 happens with +[dependabot](https://github.com/dependabot), +see [configuration file](.github/dependabot.yml). # Further reading - * The [Starter Kit announcement](http://cockpit-project.org/blog/cockpit-starter-kit.html) + * The [Starter Kit announcement](https://cockpit-project.org/blog/cockpit-starter-kit.html) blog post explains the rationale for this project. - * [Cockpit Deployment and Developer documentation](http://cockpit-project.org/guide/latest/) - * [Make your project easily discoverable](http://cockpit-project.org/blog/making-a-cockpit-application.html) + * [Cockpit Deployment and Developer documentation](https://cockpit-project.org/guide/latest/) + * [Make your project easily discoverable](https://cockpit-project.org/blog/making-a-cockpit-application.html) diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index c446c94..0000000 --- a/Vagrantfile +++ /dev/null @@ -1,30 +0,0 @@ -Vagrant.configure(2) do |config| - config.vm.box = "fedora/28-cloud-base" - config.vm.network "forwarded_port", guest: 9090, host: 9090 - - if Dir.glob("dist/*").length == 0 - config.vm.post_up_message = "NOTE: Distribution directory is empty. Run `make` to see your module show up in cockpit" - end - - config.vm.synced_folder ".", "/vagrant", disabled: true - config.vm.synced_folder "dist/", "/usr/local/share/cockpit/" + File.basename(Dir.pwd), type: "rsync", create: true - - config.vm.provider "libvirt" do |libvirt| - libvirt.memory = 1024 - end - - config.vm.provider "virtualbox" do |virtualbox| - virtualbox.memory = 1024 - end - - config.vm.provision "shell", inline: <<-EOF - set -eu - - sudo dnf install -y cockpit - - printf "[WebService]\nAllowUnencrypted=true\n" > /etc/cockpit/cockpit.conf - - systemctl enable cockpit.socket - systemctl start cockpit.socket - EOF -end diff --git a/build.js b/build.js new file mode 100755 index 0000000..55a7a4f --- /dev/null +++ b/build.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import os from 'node:os'; + +import copy from 'esbuild-plugin-copy'; + +import { cleanPlugin } from './pkg/lib/esbuild-cleanup-plugin.js'; +import { cockpitCompressPlugin } from './pkg/lib/esbuild-compress-plugin.js'; +import { cockpitPoEsbuildPlugin } from './pkg/lib/cockpit-po-plugin.js'; +import { cockpitRsyncEsbuildPlugin } from './pkg/lib/cockpit-rsync-plugin.js'; +import { esbuildStylesPlugins } from './pkg/lib/esbuild-common.js'; + +const production = process.env.NODE_ENV === 'production'; +const useWasm = os.arch() !== 'x64'; +const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default; + +const parser = (await import('argparse')).default.ArgumentParser(); +parser.add_argument('-r', '--rsync', { help: "rsync bundles to ssh target after build", metavar: "HOST" }); +parser.add_argument('-w', '--watch', { action: 'store_true', help: "Enable watch mode", default: process.env.ESBUILD_WATCH === "true" }); +parser.add_argument('-m', '--metafile', { help: "Enable bundle size information file", metavar: "FILE" }); +const args = parser.parse_args(); + +if (args.rsync) + process.env.RSYNC = args.rsync; + +// List of directories to use when using import statements +const nodePaths = ['pkg/lib']; +const outdir = 'dist'; + +// Obtain package name from package.json +const packageJson = JSON.parse(fs.readFileSync('package.json')); + +function notifyEndPlugin() { + return { + name: 'notify-end', + setup(build) { + let startTime; + + build.onStart(() => { + startTime = new Date(); + }); + + build.onEnd(() => { + const endTime = new Date(); + const timeStamp = endTime.toTimeString().split(' ')[0]; + console.log(`${timeStamp}: Build finished in ${endTime - startTime} ms`); + }); + } + }; +} + +// similar to fs.watch(), but recursively watches all subdirectories +function watch_dirs(dir, on_change) { + const callback = (ev, dir, fname) => { + // only listen for "change" events, as renames are noisy + // ignore hidden files + if (ev !== "change" || fname.startsWith('.')) { + return; + } + on_change(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()) + watch_dirs(path.join(dir, dirent.name), on_change); + } + d.closeSync(); +} + +const context = await esbuild.context({ + ...!production ? { sourcemap: "linked" } : {}, + bundle: true, + entryPoints: ['./src/index.js'], + external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts + legalComments: 'external', // Move all legal comments to a .LEGAL.txt file + loader: { ".js": "jsx" }, + metafile: !!args.metafile, + minify: production, + nodePaths, + outdir, + target: ['es2020'], + 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: ['./src/manifest.json'], to: ['./manifest.json'] }, + { from: ['./src/index.html'], to: ['./index.html'] }, + ] + }), + ...esbuildStylesPlugins, + cockpitPoEsbuildPlugin(), + ...production ? [cockpitCompressPlugin()] : [], + cockpitRsyncEsbuildPlugin({ dest: packageJson.name }), + notifyEndPlugin(), + ] +}); + +try { + const result = await context.rebuild(); + if (args.metafile) { + fs.writeFileSync(args.metafile, JSON.stringify(result.metafile)); + } +} catch (e) { + if (!args.watch) + process.exit(1); + // ignore errors in watch mode +} + +if (args.watch) { + const on_change = async path => { + console.log("change detected:", path); + await context.cancel(); + + try { + await context.rebuild(); + } catch (e) {} // ignore in watch mode + }; + + watch_dirs('src', on_change); + + // wait forever until Control-C + await new Promise(() => {}); +} + +context.dispose(); diff --git a/cockpit-starter-kit.spec.in b/cockpit-starter-kit.spec.in deleted file mode 100644 index 53e8b70..0000000 --- a/cockpit-starter-kit.spec.in +++ /dev/null @@ -1,25 +0,0 @@ -Name: cockpit-starter-kit -Version: @VERSION@ -Release: 1%{?dist} -Summary: Cockpit Starter Kit Example Module -License: LGPLv2+ - -Source: cockpit-starter-kit-%{version}.tar.gz -BuildArch: noarch - -%define debug_package %{nil} - -%description -Cockpit Starter Kit Example Module - -%prep -%setup -n cockpit-starter-kit - -%install -%make_install - -%files -%{_datadir}/cockpit/* -%{_datadir}/metainfo/* - -%changelog diff --git a/cockpituous-release b/cockpituous-release deleted file mode 100644 index b876779..0000000 --- a/cockpituous-release +++ /dev/null @@ -1,31 +0,0 @@ -# This is a script run to release welder-web through Cockpituous: -# https://github.com/cockpit-project/cockpituous/tree/master/release - -# Anything that start with 'job' may run in a way that it SIGSTOP's -# itself when preliminary preparition and then gets a SIGCONT in -# order to complete its work. -# -# Check cockpituous documentation for available release targets. - -RELEASE_SOURCE="_release/source" -RELEASE_SPEC="cockpit-starter-kit.spec" -RELEASE_SRPM="_release/srpm" - -job release-source -job release-srpm - -# Once you have a Fedora package and add the https://pagure.io/user/cockpit -# user to your project's maintainers, you can also upload to Fedora automatically: - -## Authenticate for pushing into Fedora dist-git (works in Cockpituous release container) -# cat ~/.fedora-password | kinit cockpit@FEDORAPROJECT.ORG -## Do fedora builds for the tag, using tarball -# job release-koji -k master -# job release-koji f29 -# job release-bodhi F29 - -# These are likely the first of your release targets; but run them after Fedora uploads, -# so that failures there will fail the release early, before publishing on GitHub - -# job release-github -# job release-copr @myorg/myrepo diff --git a/org.cockpit-project.starter-kit.metainfo.xml b/org.cockpit-project.starter-kit.metainfo.xml deleted file mode 100644 index ad720d8..0000000 --- a/org.cockpit-project.starter-kit.metainfo.xml +++ /dev/null @@ -1,15 +0,0 @@ - - org.cockpit-project.starter-kit - CC0-1.0 - Starter Kit - - Scaffolding for a cockpit module. - - -

- Scaffolding for a cockpit module. -

-
- cockpit.desktop - cockpit-starter-kit -
diff --git a/org.cockpit_project.starter_kit.metainfo.xml b/org.cockpit_project.starter_kit.metainfo.xml new file mode 100644 index 0000000..4430d28 --- /dev/null +++ b/org.cockpit_project.starter_kit.metainfo.xml @@ -0,0 +1,23 @@ + + + org.cockpit_project.starter_kit + CC0-1.0 + Starter Kit + Scaffolding for a cockpit module + +

+ Scaffolding for a cockpit module. + + This is just a demo which does not do much. Please replace + this with a real description. +

+
+ org.cockpit_project.cockpit + starter-kit + https://github.com/cockpit-project/starter-kit + https://github.com/cockpit-project/starter-kit/issues + cockpit-devel_AT_lists.fedorahosted.org + + Cockpit Project + +
diff --git a/package.json b/package.json index ae2f5a8..1010855 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,59 @@ { "name": "starter-kit", - "version": "0.1.0", "description": "Scaffolding for a cockpit module", + "type": "module", "main": "index.js", "repository": "git@github.com:cockpit/starter-kit.git", "author": "", "license": "LGPL-2.1", + "engines": { + "node": ">= 16" + }, "scripts": { - "build": "webpack", - "eslint": "eslint --ext .jsx --ext .es6 src/", - "eslint:fix": "eslint --fix --ext .jsx --ext .es6 src/" + "watch": "ESBUILD_WATCH='true' ./build.js", + "build": "./build.js", + "eslint": "eslint src/", + "eslint:fix": "eslint --fix src/", + "stylelint": "stylelint src/*{.css,scss}", + "stylelint:fix": "stylelint --fix src/*{.css,scss}" }, "devDependencies": { - "@babel/core": "^7.0.0", - "@babel/preset-env": "^7.0.0", - "@babel/preset-react": "^7.0.0", - "babel-eslint": "^9.0.0", - "babel-loader": "^8.0.0", - "chrome-remote-interface": "^0.25.5", - "compression-webpack-plugin": "^1.1.11", - "copy-webpack-plugin": "^4.5.2", - "css-loader": "^0.28.11", - "eslint": "^5.4.0", - "eslint-config-standard": "^11.0.0", - "eslint-config-standard-react": "^6.0.0", - "eslint-loader": "^2.1.0", - "eslint-plugin-flowtype": "^2.50.0", - "eslint-plugin-import": "^2.14.0", - "eslint-plugin-node": "^7.0.1", - "eslint-plugin-promise": "^4.0.0", - "eslint-plugin-react": "^6.9.0", - "eslint-plugin-standard": "^3.1.0", - "extract-text-webpack-plugin": "^4.0.0-beta.0", - "htmlparser": "^1.7.7", - "jed": "^1.1.1", - "po2json": "^0.4.5", - "sass-loader": "^7.0.3", - "sizzle": "^2.3.3", - "stdio": "^0.2.7", - "webpack": "^4.17.1", - "webpack-cli": "^3.1.0" + "@types/react": "18.3.13", + "@types/react-dom": "18.3.1", + "@typescript-eslint/eslint-plugin": "8.38.0", + "argparse": "2.0.1", + "esbuild": "0.25.8", + "esbuild-plugin-copy": "2.1.1", + "esbuild-plugin-replace": "1.4.0", + "esbuild-sass-plugin": "3.3.1", + "esbuild-wasm": "0.25.8", + "eslint": "8.57.1", + "eslint-config-standard": "17.1.0", + "eslint-config-standard-jsx": "11.0.0", + "eslint-config-standard-react": "13.0.0", + "eslint-plugin-import": "2.32.0", + "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", + "gettext-parser": "8.0.0", + "glob": "11.0.3", + "jed": "1.1.1", + "qunit": "2.24.1", + "sass": "1.79.6", + "stylelint": "16.22.0", + "stylelint-config-recommended-scss": "15.0.1", + "stylelint-config-standard": "38.0.0", + "stylelint-config-standard-scss": "15.0.1", + "stylelint-formatter-pretty": "4.0.1", + "typescript": "5.8.3" }, "dependencies": { - "@babel/polyfill": "^7.0.0", - "node-sass": "^4.9.0", - "react": "^16.4.2", - "react-dom": "^16.4.2" + "@patternfly/patternfly": "6.1.0", + "@patternfly/react-core": "6.1.0", + "@patternfly/react-icons": "6.1.0", + "@patternfly/react-styles": "6.3.0", + "react": "18.3.1", + "react-dom": "18.3.1" } } diff --git a/packaging/arch/PKGBUILD.in b/packaging/arch/PKGBUILD.in new file mode 100644 index 0000000..8597b62 --- /dev/null +++ b/packaging/arch/PKGBUILD.in @@ -0,0 +1,15 @@ +pkgname=cockpit-starter-kit +pkgver=VERSION +pkgrel=1 +pkgdesc='Cockpit Starter Kit Example Module' +arch=('x86_64') +url='https://github.com/cockpit-project/starter-kit' +license=(LGPL) +source=("SOURCE") +sha256sums=('SKIP') + +package() { + depends=(cockpit) + cd $pkgname + make DESTDIR="$pkgdir" install PREFIX=/usr +} diff --git a/packaging/cockpit-starter-kit.spec.in b/packaging/cockpit-starter-kit.spec.in new file mode 100644 index 0000000..e9a650f --- /dev/null +++ b/packaging/cockpit-starter-kit.spec.in @@ -0,0 +1,64 @@ +Name: cockpit-starter-kit +Version: %{VERSION} +Release: 1%{?dist} +Summary: Cockpit Starter Kit Example Module +License: LGPL-2.1-or-later + +Source0: https://github.com/cockpit-project/starter-kit/releases/download/%{version}/%{name}-%{version}.tar.xz +Source1: https://github.com/cockpit-project/starter-kit/releases/download/%{version}/%{name}-node-%{version}.tar.xz +BuildArch: noarch +%if ! 0%{?suse_version} +ExclusiveArch: %{nodejs_arches} noarch +%endif +%if ! 0%{?rhel} || 0%{?rhel} >= 10 +BuildRequires: nodejs >= 18 +%endif +BuildRequires: make +%if 0%{?suse_version} +# Suse's package has a different name +BuildRequires: appstream-glib +%else +BuildRequires: libappstream-glib +%endif +BuildRequires: gettext +%if 0%{?rhel} && 0%{?rhel} <= 8 +BuildRequires: libappstream-glib-devel +%endif + +Requires: cockpit-bridge + +%{NPM_PROVIDES} + +%description +Cockpit Starter Kit Example Module + +%prep +%autosetup -n %{name} -a 1 +# ignore pre-built bundle in release tarball and rebuild it +# but keep it in RHEL/CentOS-8/9, as that has a too old nodejs +%if ! 0%{?rhel} || 0%{?rhel} >= 10 +rm -rf dist +%endif + +%build +NODE_ENV=production make + +%install +%make_install PREFIX=/usr + +# drop source maps, they are large and just for debugging +find %{buildroot}%{_datadir}/cockpit/ -name '*.map' | xargs --no-run-if-empty rm --verbose + +%check +appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/* + +# this can't be meaningfully tested during package build; tests happen through +# FMF (see plans/all.fmf) during package gating + +%files +%doc README.md +%license LICENSE dist/index.js.LEGAL.txt +%{_datadir}/cockpit/* +%{_datadir}/metainfo/* + +%changelog diff --git a/packit.yaml b/packit.yaml new file mode 100644 index 0000000..cb24629 --- /dev/null +++ b/packit.yaml @@ -0,0 +1,62 @@ +# Enable RPM builds and running integration tests in PRs through https://packit.dev/ +# To use this, enable Packit-as-a-service in GitHub: https://packit.dev/docs/packit-as-a-service/ +# See https://packit.dev/docs/configuration/ for the format of this file + +specfile_path: cockpit-starter-kit.spec +# use the nicely formatted release description from our upstream release, instead of git shortlog +copy_upstream_release_description: true + +srpm_build_deps: + - make + - nodejs-npm + +actions: + post-upstream-clone: + - make cockpit-starter-kit.spec + # replace Source1 manually, as create-archive: can't handle multiple tarballs + - make node-cache + - sh -c 'sed -i "/^Source1:/ s/https:.*/$(ls *-node*.tar.xz)/" cockpit-*.spec' + create-archive: make dist + # starter-kit.git has no release tags; your project can drop this once you have a release + get-current-version: make print-version + +jobs: + - job: tests + trigger: pull_request + targets: &test_targets + - fedora-all + - fedora-latest-aarch64 + - centos-stream-9 + - centos-stream-9-aarch64 + - centos-stream-10 + + - job: copr_build + trigger: pull_request + targets: *test_targets + + # Build releases in COPR: https://packit.dev/docs/configuration/#copr_build + #- job: copr_build + # trigger: release + # owner: your_copr_login + # project: your_copr_project + # preserve_project: True + # targets: + # - fedora-all + # - centos-stream-9-x86_64 + + # Build releases in Fedora: https://packit.dev/docs/configuration/#propose_downstream + #- job: propose_downstream + # trigger: release + # dist_git_branches: + # - fedora-all + + #- job: koji_build + # trigger: commit + # dist_git_branches: + # - fedora-all + + #- job: bodhi_update + # trigger: commit + # dist_git_branches: + # # rawhide updates are created automatically + # - fedora-branched diff --git a/plans/all.fmf b/plans/all.fmf new file mode 100644 index 0000000..689393e --- /dev/null +++ b/plans/all.fmf @@ -0,0 +1,10 @@ +summary: + Run all tests +discover: + how: fmf +execute: + how: tmt + +# Let's handle them upstream only, don't break Fedora/RHEL reverse dependency gating +environment: + TEST_AUDIT_NO_SELINUX: 1 diff --git a/po/de.po b/po/de.po index d117b0b..0394e91 100644 --- a/po/de.po +++ b/po/de.po @@ -4,24 +4,33 @@ msgid "" msgstr "" "Project-Id-Version: starter-kit 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-29 00:14+0200\n" +"POT-Creation-Date: 2022-03-09 16:09+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" -"Language: \n" +"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1\n" #: src/index.html:20 msgid "Cockpit Starter Kit" msgstr "Cockpit Bausatz" -#: src/app.jsx:42 +#: src/app.jsx:43 msgid "Running on $0" msgstr "Läuft auf $0" -#: src/manifest.json +#: org.cockpit-project.starter-kit.metainfo.xml:6 +msgid "Scaffolding for a cockpit module" +msgstr "Gerüst für ein Cockpit-Modul" + +#: org.cockpit-project.starter-kit.metainfo.xml:8 +msgid "Scaffolding for a cockpit module." +msgstr "Gerüst für ein Cockpit-Modul." + +#: src/manifest.json:0 org.cockpit-project.starter-kit.metainfo.xml:5 msgid "Starter Kit" msgstr "Bausatz" diff --git a/po/html2po b/po/html2po deleted file mode 100755 index 2c92897..0000000 --- a/po/html2po +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env node - -/* - * Extracts translatable strings from HTML files in the following forms: - * - * String - * String - * String - * - * - * Supports the following Glade compatible forms: - * - * String - * String - * - * Supports the following angular-gettext compatible forms: - * - * String - * Singular - * - * Note that some of the use of the translated may not support all the strings - * depending on the code actually using these strings to translate the HTML. - */ - - -function fatal(message, code) { - console.log((filename || "html2po") + ": " + message); - process.exit(code || 1); -} - -function usage() { - console.log("usage: html2po input output"); - process.exit(2); -} - -var fs, htmlparser, path, stdio; - -try { - fs = require('fs'); - path = require('path'); - htmlparser = require('htmlparser'); - stdio = require('stdio'); -} catch (ex) { - fatal(ex.message, 127); /* missing looks for this */ -} - -var opts = stdio.getopt({ - directory: { key: "d", args: 1, description: "Base directory for input files" }, - output: { key: "o", args: 1, description: "Output file" }, - from: { key: "f", args: 1, description: "File containing list of input files" }, -}); - -if (!opts.from && opts.args.length < 1) { - usage(); -} - -var input = opts.args; -var entries = { }; - -/* Filename being parsed and offset of line number */ -var filename = null; -var offsets = 0; - -/* The HTML parser we're using */ -var handler = new htmlparser.DefaultHandler(function(error, dom) { - if (error) - fatal(error); - else - walk(dom); -}); - -prepare(); - -/* Decide what input files to process */ -function prepare() { - if (opts.from) { - fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - input = data.split("\n").filter(function(value) { - return !!value; - }).concat(input); - step(); - }); - } else { - step(); - } -} - -/* Now process each file in turn */ -function step() { - filename = input.shift(); - if (filename === undefined) { - finish(); - return; - } - - /* Qualify the filename if necessary */ - var full = filename; - if (opts.directory) - full = path.join(opts.directory, filename); - - fs.readFile(full, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - - var parser = new htmlparser.Parser(handler, { includeLocation: true }); - parser.parseComplete(data); - step(); - }); -} - -/* Process an array of nodes */ -function walk(children) { - if (!children) - return; - - children.forEach(function(child) { - var line = (child.location || { }).line || 0; - var offset = line - 1; - - /* Scripts get their text processed as HTML */ - if (child.type == 'script' && child.children) { - var parser = new htmlparser.Parser(handler, { includeLocation: true }); - - /* Make note of how far into the outer HTML file we are */ - offsets += offset; - - child.children.forEach(function(node) { - parser.parseChunk(node.raw); - }); - parser.done(); - - offsets -= offset; - - /* Tags get extracted as usual */ - } else if (child.type == 'tag') { - tag(child); - } - }); -} - -/* Process a single loaded tag */ -function tag(node) { - - var tasks, line, entry; - var attrs = node.attribs || { }; - var nest = true; - - /* Extract translate strings */ - if ("translate" in attrs || "translatable" in attrs) { - tasks = (attrs["translate"] || attrs["translatable"] || "yes").split(" "); - - /* Calculate the line location taking into account nested parsing */ - line = (node.location || { })["line"] || 0; - line += offsets; - - entry = { - msgctxt: attrs['translate-context'] || attrs['context'], - msgid_plural: attrs['translate-plural'], - locations: [ filename + ":" + line ] - }; - - /* For each thing listed */ - tasks.forEach(function(task) { - var copy = Object.assign({}, entry); - - /* The element text itself */ - if (task == "yes" || task == "translate") { - copy.msgid = extract(node.children); - nest = false; - - /* An attribute */ - } else if (task) { - copy.msgid = attrs[task]; - } - - if (copy.msgid) - push(copy); - }); - } - - /* Walk through all the children */ - if (nest) - walk(node.children); -} - -/* Push an entry onto the list */ -function push(entry) { - var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt; - var prev = entries[key]; - if (prev) { - prev.locations = prev.locations.concat(entry.locations); - } else { - entries[key] = entry; - } -} - -/* Extract the given text */ -function extract(children) { - if (!children) - return null; - - var i, len, node, str = []; - children.forEach(function(node) { - if (node.type == 'tag' && node.children) - str.push(extract(node.children)) - else if (node.type == 'text' && node.data) - str.push(node.data); - }); - - return str.join(""); -} - -/* Escape a string for inclusion in po file */ -function escape(string) { - var bs = string.split('\\').join('\\\\').split('"').join('\\"'); - return bs.split("\n").map(function(line) { - return '"' + line + '"'; - }).join("\n"); -} - -/* Finish by writing out the strings */ -function finish() { - var result = [ - 'msgid ""', - 'msgstr ""', - '"Project-Id-Version: PACKAGE_VERSION\\n"', - '"MIME-Version: 1.0\\n"', - '"Content-Type: text/plain; charset=UTF-8\\n"', - '"Content-Transfer-Encoding: 8bit\\n"', - '"X-Generator: Cockpit html2po\\n"', - '', - ]; - - var msgid, entry; - for (msgid in entries) { - entry = entries[msgid]; - result.push('#: ' + entry.locations.join(" ")); - if (entry.msgctxt) - result.push('msgctxt ' + escape(entry.msgctxt)); - result.push('msgid ' + escape(entry.msgid)); - if (entry.msgid_plural) { - result.push('msgid_plural ' + escape(entry.msgid_plural)); - result.push('msgstr[0] ""'); - result.push('msgstr[1] ""'); - } else { - result.push('msgstr ""'); - } - result.push(''); - } - - var data = result.join('\n'); - if (!opts.output) { - process.stdout.write(data); - process.exit(0); - } else { - fs.writeFile(opts.output, data, function(err) { - if (err) - fatal(err.message); - process.exit(0); - }); - } -} diff --git a/po/manifest2po b/po/manifest2po deleted file mode 100755 index 4d26db3..0000000 --- a/po/manifest2po +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env node - -/* - * Extracts translatable strings from manifest.json files. - * - */ - -function fatal(message, code) { - console.log((filename || "manifest2po") + ": " + message); - process.exit(code || 1); -} - -function usage() { - console.log("usage: manifest2po [-o output] input..."); - process.exit(2); -} - -var fs, path, stdio; - -try { - fs = require('fs'); - path = require('path'); - stdio = require('stdio'); -} catch (ex) { - fatal(ex.message, 127); /* missing looks for this */ -} - -var opts = stdio.getopt({ - directory: { key: "d", args: 1, description: "Base directory for input files" }, - output: { key: "o", args: 1, description: "Output file" }, - from: { key: "f", args: 1, description: "File containing list of input files" }, -}); - -if (!opts.from && opts.args.length < 1) { - usage(); -} - -var input = opts.args; -var entries = { }; - -/* Filename being parsed */ -var filename = null; - -prepare(); - -/* Decide what input files to process */ -function prepare() { - if (opts.from) { - fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - input = data.split("\n").filter(function(value) { - return !!value; - }).concat(input); - step(); - }); - } else { - step(); - } -} - -/* Now process each file in turn */ -function step() { - filename = input.shift(); - if (filename === undefined) { - finish(); - return; - } - - if (path.basename(filename) != "manifest.json") - return step(); - - fs.readFile(filename, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - - process_manifest(JSON.parse(data)); - - return step(); - }); -} - -function process_manifest(manifest) { - if (manifest.menu) - process_menu(manifest.menu); - if (manifest.tools) - process_menu(manifest.tools); -} - -function process_menu(menu) { - for (var m in menu) { - if (menu[m].label) { - push({ - msgid: menu[m].label, - locations: [ filename ] - }); - } - } -} - -/* Push an entry onto the list */ -function push(entry) { - var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt; - var prev = entries[key]; - if (prev) { - prev.locations = prev.locations.concat(entry.locations); - } else { - entries[key] = entry; - } -} - -/* Escape a string for inclusion in po file */ -function escape(string) { - var bs = string.split('\\').join('\\\\').split('"').join('\\"'); - return bs.split("\n").map(function(line) { - return '"' + line + '"'; - }).join("\n"); -} - -/* Finish by writing out the strings */ -function finish() { - var result = [ - 'msgid ""', - 'msgstr ""', - '"Project-Id-Version: PACKAGE_VERSION\\n"', - '"MIME-Version: 1.0\\n"', - '"Content-Type: text/plain; charset=UTF-8\\n"', - '"Content-Transfer-Encoding: 8bit\\n"', - '"X-Generator: Cockpit manifest2po\\n"', - '', - ]; - - var msgid, entry; - for (msgid in entries) { - entry = entries[msgid]; - result.push('#: ' + entry.locations.join(" ")); - if (entry.msgctxt) - result.push('msgctxt ' + escape(entry.msgctxt)); - result.push('msgid ' + escape(entry.msgid)); - if (entry.msgid_plural) { - result.push('msgid_plural ' + escape(entry.msgid_plural)); - result.push('msgstr[0] ""'); - result.push('msgstr[1] ""'); - } else { - result.push('msgstr ""'); - } - result.push(''); - } - - var data = result.join('\n'); - if (!opts.output) { - process.stdout.write(data); - process.exit(0); - } else { - fs.writeFile(opts.output, data, function(err) { - if (err) - fatal(err.message); - process.exit(0); - }); - } -} diff --git a/po/po.empty.js b/po/po.empty.js deleted file mode 100644 index b27dad9..0000000 --- a/po/po.empty.js +++ /dev/null @@ -1,14 +0,0 @@ -(function (root, data) { - var loaded, module; - - /* Load into Cockpit locale */ - if (typeof cockpit === 'object') { - cockpit.locale(data) - loaded = true; - } - - if (!loaded) - root.po = data; - -/* The syntax of this line is important by po2json */ -}(this, {"":{"language":"en"}})); diff --git a/po/po2json b/po/po2json deleted file mode 100755 index d2185a4..0000000 --- a/po/po2json +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env node - -function fatal(message, code) { - console.log((filename || "html2po") + ": " + message); - process.exit(code || 1); -} - -function usage() { - console.log("usage: po2json [--module=template.js] input output"); - process.exit(2); -} - -var fs, po2json, Jed, stdio; - -try { - fs = require('fs'); - po2json = require('po2json'); - Jed = require('jed'); - stdio = require('stdio'); -} catch(ex) { - fatal(ex.message, 127); /* missing looks for this */ -} - -var argi = 2; -var filename = null; - -var opts = stdio.getopt({ - module: { key: "m", args: 1, description: "Module template to include" }, - output: { key: "o", args: 1, description: "Output file" }, -}); - -if (opts.args.length != 1) { - usage(); -} - -parse(); - -function prepareHeader(header) { - var body, statement, plurals = header["plural-forms"], ret = null; - if (plurals) { - try { - /* Check that the plural forms isn't being sneaky since we build a function here */ - Jed.PF.parse(plurals); - } catch(ex) { - fatal("bad plural forms: " + ex.message, 1); - } - - /* A function for the front end */ - statement = header["plural-forms"]; - if (statement[statement.length - 1] != ';') - statement += ';'; - ret = 'function(n) {\nvar nplurals, plural;\n' + statement + '\nreturn plural;\n}'; - - /* Added back in later */ - delete header["plural-forms"]; - } - - /* We don't need to be transferring this */ - delete header["project-id-version"]; - delete header["report-msgid-bugs-to"]; - delete header["pot-creation-date"]; - delete header["po-revision-date"]; - delete header["last-translator"]; - delete header["language-team"]; - delete header["mime-version"]; - delete header["content-type"]; - delete header["content-transfer-encoding"]; - - return ret; -} - -/* Parse and process the po data */ -function parse() { - filename = opts.args[0]; - po2json.parseFile(opts.args[0], { "fuzzy": true }, function(err, jsonData) { - var plurals, pos; - - if (err) - fatal(err.message); - - var header = jsonData[""]; - if (header) - plurals = prepareHeader(header); - - var data = JSON.stringify(jsonData, null, 1); - - /* We know the brace in is the location to insert our function */ - if (plurals) { - pos = data.indexOf('{', 1); - data = data.substr(0, pos + 1) + "'plural-forms':" + String(plurals) + "," + data.substr(pos + 1); - } - - if (data == JSON.stringify({})) - finish(""); - else - wrap(data); - }); -} - -/* Wrap the data if desired */ -function wrap(data) { - if (opts.module) { - filename = opts.module; - fs.readFile(opts.module, { encoding: "utf-8" }, function(err, template) { - if (err) - fatal(err.message); - data = template.replace('{"":{"language":"en"}}', data); - finish(data); - }); - } else { - finish(data); - } -} - -/* Write it out */ -function finish(data) { - if (opts.output) { - fs.writeFile(opts.output, data, function(err) { - if (err) - fatal(err.message); - process.exit(0); - }); - } else { - process.stdout.write(data); - process.exit(0); - } -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8963df6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[tool.mypy] +follow_imports = 'silent' # https://github.com/python-lsp/pylsp-mypy/issues/81 +scripts_are_modules = true # allow checking all scripts in one invocation +explicit_package_bases = true +mypy_path = 'test/common:test:bots' +exclude = [ + "bots" +] + +[[tool.mypy.overrides]] +ignore_missing_imports = true +module = [ + # run without bots checked out + "machine.*", + "testvm", + + # run without gobject-introspection + "gi.*", +] + +[tool.ruff] +exclude = [ + ".git/", + "modules/", + "node_modules/", +] +line-length = 118 +src = [] + +[tool.ruff.lint] +select = [ + "A", # flake8-builtins + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D300", # pydocstyle: Forbid ''' in docstrings + "E", # pycodestyle + "EXE", # flake8-executable + "F", # pyflakes + "FBT", # flake8-boolean-trap + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "PLE", # pylint errors + "PGH", # pygrep-hooks + "RSE", # flake8-raise + "RUF", # ruff rules + "T10", # flake8-debugger + "TCH", # flake8-type-checking + "UP032", # f-string + "W", # warnings (mostly whitespace) + "YTT", # flake8-2020 +] +ignore = [ + "FBT002", # Boolean default value in function definition + "FBT003", # Boolean positional value in function call +] + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.isort] +known-first-party = ["cockpit"] + +[tool.vulture] +ignore_names = [ + "test[A-Z0-9]*", +] diff --git a/src/app.jsx b/src/app.jsx deleted file mode 100644 index 55f1c88..0000000 --- a/src/app.jsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of Cockpit. - * - * Copyright (C) 2017 Red Hat, Inc. - * - * Cockpit 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. - * - * Cockpit 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 Cockpit; If not, see . - */ - -import cockpit from 'cockpit'; -import React from 'react'; -import './app.scss'; - -const _ = cockpit.gettext; - -export class Application extends React.Component { - constructor() { - super(); - this.state = { 'hostname': _("Unknown") }; - - cockpit.file('/etc/hostname').read() - .done((content) => { - this.setState({ 'hostname': content.trim() }); - }); - } - - render() { - return ( -
-

Starter Kit

-

- { cockpit.format(_("Running on $0"), this.state.hostname) } -

-
- ); - } -} diff --git a/src/app.scss b/src/app.scss index 87b46f4..6d2c5d8 100644 --- a/src/app.scss +++ b/src/app.scss @@ -1,3 +1,5 @@ +@use "page.scss"; + p { font-weight: bold; } diff --git a/src/app.tsx b/src/app.tsx new file mode 100644 index 0000000..0d3b12f --- /dev/null +++ b/src/app.tsx @@ -0,0 +1,48 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2017 Red Hat, Inc. + * + * Cockpit 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. + * + * Cockpit 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 Cockpit; If not, see . + */ + +import React, { useEffect, useState } from 'react'; +import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; +import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js"; + +import cockpit from 'cockpit'; + +const _ = cockpit.gettext; + +export const Application = () => { + const [hostname, setHostname] = useState(_("Unknown")); + + useEffect(() => { + const hostname = cockpit.file('/etc/hostname'); + hostname.watch(content => setHostname(content?.trim() ?? "")); + return hostname.close; + }, []); + + return ( + + Starter Kit + + + + + ); +}; diff --git a/src/index.html b/src/index.html index 5182d9b..2b06bb4 100644 --- a/src/index.html +++ b/src/index.html @@ -17,17 +17,15 @@ along with this package; If not, see . --> - Cockpit Starter Kit + Cockpit Starter Kit - - - + diff --git a/src/index.es6 b/src/index.tsx similarity index 74% rename from src/index.es6 rename to src/index.tsx index cb3ca05..c01c34c 100644 --- a/src/index.es6 +++ b/src/index.tsx @@ -18,9 +18,15 @@ */ import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; + +import "cockpit-dark-theme"; + import { Application } from './app.jsx'; -document.addEventListener("DOMContentLoaded", function () { - ReactDOM.render(React.createElement(Application, {}), document.getElementById('app')); +import "patternfly/patternfly-6-cockpit.scss"; +import './app.scss'; + +document.addEventListener("DOMContentLoaded", () => { + createRoot(document.getElementById("app")!).render(); }); diff --git a/src/manifest.json b/src/manifest.json index 3e2454a..3a45f56 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,5 +1,4 @@ { - "version": "0.1", "requires": { "cockpit": "137" }, diff --git a/test/browser/browser.sh b/test/browser/browser.sh new file mode 100755 index 0000000..6a03a9e --- /dev/null +++ b/test/browser/browser.sh @@ -0,0 +1,46 @@ +set -eux + +cd "${0%/*}/../.." + +# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2033020 +dnf update -y pam || true + +# allow test to set up things on the machine +mkdir -p /root/.ssh +curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys +chmod 600 /root/.ssh/authorized_keys + +# create user account for logging in +if ! id admin 2>/dev/null; then + useradd -c Administrator -G wheel admin + echo admin:foobar | chpasswd +fi + +# set root's password +echo root:foobar | chpasswd + +# avoid sudo lecture during tests +su -c 'echo foobar | sudo --stdin whoami' - admin + +# disable core dumps, we rather investigate them upstream where test VMs are accessible +echo core > /proc/sys/kernel/core_pattern + +sh test/vm.install + +# Run tests in the cockpit tasks container, as unprivileged user +CONTAINER="$(cat .cockpit-ci/container)" +if grep -q platform:el10 /etc/os-release; then + # HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2273078 + export NETAVARK_FW=nftables +fi +exec podman \ + run \ + --rm \ + --shm-size=1024m \ + --security-opt=label=disable \ + --env='TEST_*' \ + --volume="${TMT_TEST_DATA}":/logs:rw,U --env=LOGS=/logs \ + --volume="$(pwd)":/source:rw,U --env=SOURCE=/source \ + --volume=/usr/lib/os-release:/run/host/usr/lib/os-release:ro \ + "${CONTAINER}" \ + sh /source/test/browser/run-test.sh diff --git a/test/browser/main.fmf b/test/browser/main.fmf new file mode 100644 index 0000000..1e575ab --- /dev/null +++ b/test/browser/main.fmf @@ -0,0 +1,10 @@ +summary: + Run browser integration tests on the host +require: + - cockpit-starter-kit + - podman + - cockpit-ws + - cockpit-system + - glibc-langpack-de +test: ./browser.sh +duration: 60m diff --git a/test/browser/run-test.sh b/test/browser/run-test.sh new file mode 100644 index 0000000..586583d --- /dev/null +++ b/test/browser/run-test.sh @@ -0,0 +1,40 @@ +set -eux + +cd "${SOURCE}" + +# tests need cockpit's bots/ libraries and test infrastructure +git init +rm -f bots # common local case: existing bots symlink +make bots test/common + +# disable detection of affected tests; testing takes too long as there is no parallelization +mv .git dot-git + +. /run/host/usr/lib/os-release +export TEST_OS="${ID}-${VERSION_ID/./-}" + +if [ "$TEST_OS" = "centos-9" ]; then + TEST_OS="${TEST_OS}-stream" +fi + +# Chromium sometimes gets OOM killed on testing farm +export TEST_BROWSER=firefox + +EXCLUDES="" + +# make it easy to check in logs +echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}" +echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}" + +GATEWAY="$(python3 -c 'import socket; print(socket.gethostbyname("_gateway"))')" +RC=0 +./test/common/run-tests \ + --nondestructive \ + --machine "${GATEWAY}":22 \ + --browser "${GATEWAY}":9090 \ + $EXCLUDES \ +|| RC=$? + +echo $RC > "$LOGS/exitcode" +cp --verbose Test* "$LOGS" || true +exit $RC diff --git a/test/check-application b/test/check-application index 40431e2..9373e41 100755 --- a/test/check-application +++ b/test/check-application @@ -1,18 +1,16 @@ -#!/usr/bin/python3 +#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv) + # Run this with --help to see available options for tracing and debugging -# See https://github.com/cockpit-project/cockpit/blob/master/test/common/testlib.py +# See https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py # "class Browser" and "class MachineCase" for the available API. -import os -import sys - -# import Cockpit's machinery for test VMs and its browser test API -TEST_DIR = os.path.dirname(__file__) -sys.path.append(os.path.join(TEST_DIR, "common")) -sys.path.append(os.path.join(os.path.dirname(TEST_DIR), "bots/machine")) import testlib +# Nondestructive tests all run in the same running VM. This allows them to run in Packit, Fedora, and +# RHEL dist-git gating. They must not permanently change any file or configuration on the system in a +# way that influences other tests. +@testlib.nondestructive class TestApplication(testlib.MachineCase): def testBasic(self): b = self.browser @@ -20,32 +18,36 @@ class TestApplication(testlib.MachineCase): self.login_and_go("/starter-kit") # verify expected heading - b.wait_present(".container-fluid h2") - b.wait_text(".container-fluid h2", "Starter Kit") + b.wait_text(".pf-v6-c-card__title", "Starter Kit") # verify expected host name - hostname = m.execute("hostname").strip() - b.wait_present(".container-fluid p") - b.wait_text(".container-fluid p", "Running on " + hostname) + hostname = m.execute("cat /etc/hostname").strip() + b.wait_in_text(".pf-v6-c-alert__title", "Running on " + hostname) + + # change current hostname + self.write_file("/etc/hostname", "new-" + hostname) + # verify new hostname name + b.wait_in_text(".pf-v6-c-alert__title", "Running on new-" + hostname) # change language to German b.switch_to_top() - b.click("#content-user-name") - b.click(".display-language-menu a") - b.wait_popup('display-language') - b.set_val("#display-language select", "de-de") - b.click("#display-language-select-button") - b.expect_load() - # HACK: work around language switching in Chrome not working in current session (Cockpit issue #8160) - b.reload(ignore_cache=True) - b.wait_present("#content") + # the menu and dialog changed several times + b.click("#toggle-menu") + b.click("button.display-language-menu") + b.wait_popup('display-language-modal') + b.click("#display-language-modal [data-value='de-de'] button") + b.click("#display-language-modal button.pf-m-primary") + b.wait_visible("#content") # menu label (from manifest) should be translated b.wait_text("#host-apps a[href='/starter-kit']", "Bausatz") + # window title should be translated; this is not considered as "visible" + self.assertIn("Bausatz", b.call_js_func("ph_text", "head title")) b.go("/starter-kit") b.enter_page("/starter-kit") # page label (from js) should be translated - b.wait_in_text(".container-fluid p", "Läuft auf") + b.wait_in_text(".pf-v6-c-alert__title", "Läuft auf") + if __name__ == '__main__': testlib.test_main() diff --git a/test/reference-image b/test/reference-image new file mode 100644 index 0000000..c06d46d --- /dev/null +++ b/test/reference-image @@ -0,0 +1 @@ +fedora-35 diff --git a/test/run b/test/run index 46a2d01..02e373f 100755 --- a/test/run +++ b/test/run @@ -1,4 +1,13 @@ -#! /bin/bash +#! /bin/sh +set -eu + # This is the expected entry point for Cockpit CI; will be called without -# arguments but with an appropriate $TEST_OS +# arguments but with an appropriate $TEST_OS, and optionally $TEST_SCENARIO + +TEST_SCENARIO="${TEST_SCENARIO:-}" +[ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox +export RUN_TESTS_OPTIONS=--track-naughties + +make codecheck make check +make po/starter-kit.pot diff --git a/test/vm.install b/test/vm.install index 55e548d..84f26f7 100644 --- a/test/vm.install +++ b/test/vm.install @@ -1,12 +1,13 @@ #!/bin/sh -# image-customize script to enable cockpit in test VMs -# The application RPM will be installed separately -set -eu +# image-customize script to prepare a bots VM for testing this application +# The application package will be installed separately +set -eux # don't force https:// (self-signed cert) +mkdir -p /etc/cockpit printf "[WebService]\\nAllowUnencrypted=true\\n" > /etc/cockpit/cockpit.conf -if type firewall-cmd >/dev/null 2>&1; then +if systemctl is-active -q firewalld.service; then firewall-cmd --add-service=cockpit --permanent fi systemctl enable cockpit.socket diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c4b1669 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "exactOptionalPropertyTypes": true, + "jsx": "react", + "lib": [ + "dom", + "es2020" + ], + "paths": { + "*": ["./pkg/lib/*"] + }, + "moduleResolution": "bundler", + "noEmit": true, + "strict": true, + "target": "es2020" + }, + "include": [ + "src/**/*" + ] +} diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index fce4863..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,141 +0,0 @@ -const path = require("path"); -const copy = require("copy-webpack-plugin"); -const extract = require("extract-text-webpack-plugin"); -const fs = require("fs"); -const webpack = require("webpack"); -const CompressionPlugin = require("compression-webpack-plugin"); - -var externals = { - "cockpit": "cockpit", -}; - -/* These can be overridden, typically from the Makefile.am */ -const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src"; -const builddir = (process.env.SRCDIR || __dirname); -const distdir = builddir + path.sep + "dist"; -const section = process.env.ONLYDIR || null; -const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules"); - -/* A standard nodejs and webpack pattern */ -var production = process.env.NODE_ENV === 'production'; - -var info = { - entries: { - "index": [ - "./index.es6" - ] - }, - files: [ - "index.html", - "manifest.json", - ], -}; - -var output = { - path: distdir, - filename: "[name].js", - sourceMapFilename: "[file].map", -}; - -/* - * Note that we're avoiding the use of path.join as webpack and nodejs - * want relative paths that start with ./ explicitly. - * - * In addition we mimic the VPATH style functionality of GNU Makefile - * where we first check builddir, and then srcdir. - */ - -function vpath(/* ... */) { - var filename = Array.prototype.join.call(arguments, path.sep); - var expanded = builddir + path.sep + filename; - if (fs.existsSync(expanded)) - return expanded; - expanded = srcdir + path.sep + filename; - return expanded; -} - -/* Qualify all the paths in entries */ -Object.keys(info.entries).forEach(function(key) { - if (section && key.indexOf(section) !== 0) { - delete info.entries[key]; - return; - } - - info.entries[key] = info.entries[key].map(function(value) { - if (value.indexOf("/") === -1) - return value; - else - return vpath(value); - }); -}); - -/* Qualify all the paths in files listed */ -var files = []; -info.files.forEach(function(value) { - if (!section || value.indexOf(section) === 0) - files.push({ from: vpath("src", value), to: value }); -}); -info.files = files; - -var plugins = [ - new copy(info.files), - new extract("[name].css") -]; - -/* Only minimize when in production mode */ -if (production) { - /* Rename output files when minimizing */ - output.filename = "[name].min.js"; - - plugins.unshift(new CompressionPlugin({ - asset: "[path].gz[query]", - test: /\.(js|html)$/, - minRatio: 0.9, - deleteOriginalAssets: true - })); -} - -module.exports = { - mode: production ? 'production' : 'development', - entry: info.entries, - externals: externals, - output: output, - devtool: "source-map", - module: { - rules: [ - { - enforce: 'pre', - exclude: /node_modules/, - loader: 'eslint-loader', - test: /\.jsx$/ - }, - { - enforce: 'pre', - exclude: /node_modules/, - loader: 'eslint-loader', - test: /\.es6$/ - }, - { - exclude: /node_modules/, - loader: 'babel-loader', - test: /\.js$/ - }, - { - exclude: /node_modules/, - loader: 'babel-loader', - test: /\.jsx$/ - }, - { - exclude: /node_modules/, - loader: 'babel-loader', - test: /\.es6$/ - }, - { - exclude: /node_modules/, - loader: extract.extract('css-loader!sass-loader'), - test: /\.scss$/ - } - ] - }, - plugins: plugins -}