From 235f110ec77a4063f59d3a5c799770c99a5b5c37 Mon Sep 17 00:00:00 2001 From: Justin Stephenson Date: Thu, 13 Apr 2023 11:28:00 -0400 Subject: [PATCH] Bring up to date with cockpit starter kit This encompasses a number of changes to the build process. --- .babelrc | 4 - .eslintignore | 1 + .eslintrc.json | 108 +++---- .gitignore | 7 +- .stylelintrc.json | 38 +++ .tasks | 13 - .travis.yml | 8 - Dockerfile | 17 -- Makefile | 221 ++++++++------- README.md | 91 +++++- Vagrantfile | 30 -- build.js | 136 +++++++++ cockpituous-release | 31 -- ...pit-project.session-recording.metainfo.xml | 11 +- package.json | 76 ++--- .../cockpit-session-recording.spec.in | 21 +- po/de.po | 15 +- po/html2po | 264 ------------------ po/manifest2po | 185 ------------ po/po.empty.js | 14 - po/po2json | 127 --------- src/app.jsx | 8 +- src/index.html | 21 +- src/index.js | 13 +- src/manifest.json | 2 +- src/player.css | 10 +- src/player.jsx | 2 +- test/check-application | 10 +- test/reference-image | 1 + test/run | 8 +- test/vm.install | 10 +- webpack.config.js | 202 -------------- 32 files changed, 533 insertions(+), 1172 deletions(-) delete mode 100644 .babelrc create mode 100644 .stylelintrc.json delete mode 100755 .tasks delete mode 100644 .travis.yml delete mode 100644 Dockerfile delete mode 100644 Vagrantfile create mode 100755 build.js delete mode 100644 cockpituous-release rename cockpit-session-recording.spec.in => packaging/cockpit-session-recording.spec.in (89%) delete mode 100755 po/html2po delete mode 100755 po/manifest2po delete mode 100644 po/po.empty.js delete mode 100755 po/po2json create mode 100644 test/reference-image delete mode 100644 webpack.config.js 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/.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 f734125..7818ff7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,67 +1,51 @@ { - "root": true, - "env": { - "browser": true, - "es6": true - }, - "extends": ["eslint:recommended", "standard", "standard-react"], - "parser": "@babel/eslint-parser", - "parserOptions": { - "ecmaVersion": "7", - "ecmaFeatures": { - "jsx": true + "root": true, + "env": { + "browser": true, + "es6": true }, - "sourceType": "module" - }, - "plugins": ["flowtype", "react", "react-hooks"], - "rules": { - "indent": [ - "error", - 4, - { - "ObjectExpression": "first", - "CallExpression": { "arguments": "first" }, - "MemberExpression": 2, - "ignoredNodes": ["JSXAttribute"] - } - ], - "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }], - "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 }], + "extends": ["eslint:recommended", "standard", "standard-jsx", "react-app"], + "parserOptions": { + "ecmaVersion": "7", + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "plugins": ["flowtype", "react", "react-hooks"], + "rules": { + "indent": ["error", 4, + { + "ObjectExpression": "first", + "CallExpression": {"arguments": "first"}, + "MemberExpression": 2, + "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", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", - "camelcase": "off", - "comma-dangle": "off", - "curly": "off", - "jsx-quotes": "off", - "key-spacing": "off", - "new-cap": "off", - "no-console": "off", - "prefer-const": "off", - "quotes": "off", - "react/jsx-closing-bracket-location": "off", - "react/jsx-curly-spacing": "off", - "react/jsx-indent-props": "off", - "react/jsx-handler-names": "off", - "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" - }, - "globals": { - "require": false, - "module": false - } + "camelcase": "off", + "comma-dangle": "off", + "curly": "off", + "jsx-quotes": "off", + "key-spacing": "off", + "no-console": "off", + "quotes": "off", + "react/jsx-curly-spacing": "off", + "react/jsx-indent-props": "off", + "react/prop-types": "off", + "space-before-function-paren": "off", + "standard/no-callback-literal": "off" + }, + "globals": { + "require": false, + "module": false + } } diff --git a/.gitignore b/.gitignore index b03e4d9..aa13142 100644 --- a/.gitignore +++ b/.gitignore @@ -4,15 +4,16 @@ *.rpm node_modules/ dist/ -lib/ /*.spec /.vagrant package-lock.json Test*FAIL* -bots/ +/bots test/common/ test/images/ +pkg *.pot POTFILES* tmp/ -.mypy_cache +/po/LINGUAS +/tools diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..352fbe6 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,38 @@ +{ + "extends": "stylelint-config-standard-scss", + "rules": { + "declaration-colon-newline-after": null, + "selector-list-comma-newline-after": null, + + "at-rule-empty-line-before": null, + "declaration-colon-space-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, + "indentation": null, + "keyframes-name-pattern": null, + "max-line-length": null, + "no-descending-specificity": null, + "no-duplicate-selectors": null, + "scss/at-extend-no-missing-placeholder": null, + "scss/at-import-partial-extension": null, + "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/.tasks b/.tasks deleted file mode 100755 index 2026460..0000000 --- a/.tasks +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -# When run automated, randomize to minimize stampeding herd -if [ -t 0 ]; then - chance=10 -else - chance=$(shuf -i 0-10 -n 1) -fi - -if [ $chance -gt 9 ]; then - # Open issues for things that need doing on a regular basis - bots/npm-trigger -fi 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/Dockerfile b/Dockerfile deleted file mode 100644 index d48497c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM fedora:35 - -COPY . cockpit-session-recording - -RUN sudo dnf -y install \ - git \ - gnupg \ - intltool \ - libappstream-glib \ - make \ - npm \ - rpm-build \ - rpmdevtools \ - rsync \ - tar - -RUN cd cockpit-session-recording && make rpm diff --git a/Makefile b/Makefile index d581439..7c126c2 100644 --- a/Makefile +++ b/Makefile @@ -3,24 +3,45 @@ PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' packa RPM_NAME := cockpit-$(PACKAGE_NAME) VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.') ifeq ($(TEST_OS),) -TEST_OS = rhel-x +TEST_OS = centos-8-stream endif export TEST_OS -TARFILE=cockpit-$(PACKAGE_NAME)-$(VERSION).tar.xz +TARFILE=$(RPM_NAME)-$(VERSION).tar.xz +NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz SPEC=$(RPM_NAME).spec -# rpmspec -q behaves differently in Fedora ≥ 37 -RPMQUERY=$(shell rpmspec -D"VERSION $(VERSION)" -q --srpm $(SPEC).in).rpm -SRPMFILE=$(subst noarch,src,$(RPMQUERY)) -RPMFILE=$(subst src,noarch,$(RPMQUERY)) +PREFIX ?= /usr/local +APPSTREAMFILE=org.cockpit-project.$(PACKAGE_NAME).metainfo.xml VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS) -# stamp file to check if/when npm install ran +# stamp file to check for node_modules/ NODE_MODULES_TEST=package-lock.json -# one example file in dist/ from webpack to check if that already ran -WEBPACK_TEST=dist/manifest.json -# one example file in src/lib to check if it was already checked out -LIB_TEST=src/lib/cockpit-po-plugin.js +# 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 \ + tools/git-utils.sh \ + tools/make-bots \ + $(NULL) + +COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git +COCKPIT_REPO_COMMIT = 6073b2703acd68e216bd9dbc116c30d2d7a9701c # 288.1 + esbuild plugin updates + +$(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 @@ -28,95 +49,98 @@ all: $(WEBPACK_TEST) LINGUAS=$(basename $(notdir $(wildcard po/*.po))) -po/POTFILES.js.in: - mkdir -p $(dir $@) - find src/ -name '*.js' -o -name '*.jsx' > $@ - -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= \ + --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 '*.js' -o -name '*.jsx') -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.js -o $@ $$(find src -name '*.html') -po/$(PACKAGE_NAME).html.pot: po/POTFILES.html.in - po/html2po -f $^ -o $@ +po/$(PACKAGE_NAME).manifest.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) + pkg/lib/manifest2po.js src/manifest.json -o $@ -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) $(LIB_TEST) $(shell find src/ -type f) package.json webpack.config.js $(patsubst %,dist/po.%.js,$(LINGUAS)) - NODE_ENV=$(NODE_ENV) node_modules/.bin/webpack +$(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_ENV=$(NODE_ENV) node_modules/.bin/webpack --watch + NODE_ENV=$(NODE_ENV) npm run watch clean: rm -rf dist/ - [ ! -e cockpit-$(PACKAGE_NAME).spec.in ] || rm -f cockpit-$(PACKAGE_NAME).spec + rm -f $(SPEC) + 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) -dist: $(TARFILE) +# 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) -# when building a distribution tarball, call webpack with a 'production' environment +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 packge-lock.json (so that +# 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): $(WEBPACK_TEST) cockpit-$(PACKAGE_NAME).spec +$(TARFILE): $(DIST_TEST) $(SPEC) if type appstream-util >/dev/null 2>&1; then appstream-util validate-relax --nonet *.metainfo.xml; fi - touch -r package.json $(NODE_MODULES_TEST) - touch dist/* - tar --xz -cf cockpit-$(PACKAGE_NAME)-$(VERSION).tar.xz --transform 's,^,cockpit-$(PACKAGE_NAME)/,' \ - --exclude cockpit-$(PACKAGE_NAME).spec.in --exclude node_modules \ - $$(git ls-files) src/lib package-lock.json cockpit-$(PACKAGE_NAME).spec dist/ + 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) dist/ -srpm: $(TARFILE) cockpit-$(PACKAGE_NAME).spec +$(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: $(RPMFILE) - -$(RPMFILE): $(TARFILE) cockpit-$(PACKAGE_NAME).spec +# convenience target for developers +rpm: $(TARFILE) $(NODE_CACHE) $(SPEC) mkdir -p "`pwd`/output" mkdir -p "`pwd`/rpmbuild" rpmbuild -bb \ @@ -126,55 +150,58 @@ $(RPMFILE): $(TARFILE) 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" - # sanity check - test -e "$(RPMFILE)" -# build a VM with locally built rpm installed -$(VM_IMAGE): $(RPMFILE) bots - rm -f $(VM_IMAGE) $(VM_IMAGE).qcow2 - bots/image-customize -v -i `pwd`/$(RPMFILE) -s $(CURDIR)/test/vm.install $(TEST_OS) - bots/image-customize -v -u ./test/files/1.journal:/var/log/journal/1.journal $(TEST_OS) - bots/image-customize -v -u ./test/files/binary-rec.journal:/var/log/journal/binary-rec.journal $(TEST_OS) +ifeq ("$(TEST_SCENARIO)","pybridge") +COCKPIT_PYBRIDGE_REF = main +COCKPIT_WHEEL = cockpit-0-py3-none-any.whl + +$(COCKPIT_WHEEL): + pip wheel git+https://github.com/cockpit-project/cockpit.git@${COCKPIT_PYBRIDGE_REF} + +VM_DEPENDS = $(COCKPIT_WHEEL) +VM_CUSTOMIZE_FLAGS = --install $(COCKPIT_WHEEL) +endif + +# 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): $(TARFILE) $(NODE_CACHE) bots test/vm.install $(VM_DEPENDS) + bots/image-customize --fresh \ + $(VM_CUSTOMIZE_FLAGS) \ + --upload $(NODE_CACHE):/var/tmp/ --build $(TARFILE) \ + --upload ./test/files/1.journal:/var/log/journal/1.journal \ + --upload ./test/files/binary-rec.journal:/var/log/journal/binary-rec.journal \ + --script $(CURDIR)/test/vm.install $(TEST_OS) # convenience target for the above vm: $(VM_IMAGE) - echo $(VM_IMAGE) + @echo $(VM_IMAGE) + +# convenience target to print the filename of the test image +print-vm: + @echo $(VM_IMAGE) + +# 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 # run the browser integration tests; skip check for SELinux denials # this will run all tests/check-* and format them as TAP -check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common - TEST_AUDIT_NO_SELINUX=1 test/common/run-tests +check: prepare-check + TEST_AUDIT_NO_SELINUX=1 test/common/run-tests ${RUN_TESTS_OPTIONS} # checkout Cockpit's bots for standard test VM images and API to launch them -# must be from master, as only that has current and existing images; but testvm.py API is stable -# support CI testing against a bots change -bots: - git clone --quiet --reference-if-able $${XDG_CACHE_HOME:-$$HOME/.cache}/cockpit-project/bots https://github.com/cockpit-project/bots.git - if [ -n "$$COCKPIT_BOTS_REF" ]; then git -C bots fetch --quiet --depth=1 origin "$$COCKPIT_BOTS_REF"; git -C bots checkout --quiet FETCH_HEAD; fi - @echo "checked out bots/ ref $$(git -C bots rev-parse HEAD)" - -# 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 release, and update it from time to time -test/common: - git fetch --depth=1 https://github.com/cockpit-project/cockpit.git 267 - git checkout --force FETCH_HEAD -- test/common - git reset test/common - -# checkout Cockpit's PF/React/build library; again this has no API stability guarantee, so check out a stable tag -$(LIB_TEST): - git clone -b 256 --depth=1 https://github.com/cockpit-project/cockpit.git tmp/cockpit - mv tmp/cockpit/pkg/lib src/ - rm -rf tmp/cockpit +bots: tools/make-bots + tools/make-bots $(NODE_MODULES_TEST): package.json # 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 + env -u NODE_ENV npm install --ignore-scripts env -u NODE_ENV npm prune -.PHONY: all clean install devel-install dist 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 8c5ad2c..853cca6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,10 @@ Demos & Talks: GitHub Organization: * [scribery.github.io](http://scribery.github.io/) - * [Scribery](https://github.com/Scribery) + * [Scribery](https://github.com/Scribery) + +This project is based on the [Cockpit Starter Kit](https://github.com/cockpit-project/starter-kit). +See [Starter Kit Intro](http://cockpit-project.org/blog/cockpit-starter-kit.html) for details. # Getting and building the source @@ -30,15 +33,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 @@ -48,12 +53,39 @@ ln -s `pwd`/dist ~/.local/share/cockpit/session-recording 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 + + $ npm run watch + +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 code style in `.js` and `.jsx` files. -The linter is executed within every build as a webpack preloader. +eslint is executed within every build. For developer convenience, the ESLint can be started explicitly by: @@ -65,6 +97,49 @@ Violations of some rules can be fixed automatically by: Rules configuration can be found in the `.eslintrc.json` file. -# Credits +## Running stylelint -Cockpit-session-recording is based on [starter-kit](http://cockpit-project.org/blog/cockpit-starter-kit.html). +Cockpit uses [Stylelint](https://stylelint.io/) to automatically check CSS code +style in `.css` and `scss` files. + +styleint is executed within every build. + +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. + +During fast iterative development, you can also choose to not run eslint/stylelint. +This speeds up the build and avoids build failures due to e. g. ill-formatted +css or other issues: + + $ make LINT=0 + +# Running tests locally + +Run `make check` to build an RPM, install it into a standard Cockpit test VM +(centos-8-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 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-8-stream test/check-application -tvs + +It is possible to setup the test environment without running the tests: + + TEST_OS=centos-8-stream make prepare-check + +You can also run the test against a different Cockpit image, for example: + + TEST_OS=fedora-34 make check 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..6b62d01 --- /dev/null +++ b/build.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +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'; +import { eslintPlugin } from './pkg/lib/esbuild-eslint-plugin.js'; +import { stylelintPlugin } from './pkg/lib/esbuild-stylelint-plugin.js'; + +const useWasm = os.arch() !== 'x64'; +const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default; + +const production = process.env.NODE_ENV === 'production'; +const watchMode = process.env.ESBUILD_WATCH === "true"; +// linters dominate the build time, so disable them for production builds by default, but enable in watch mode +const lint = process.env.LINT ? (process.env.LINT !== 0) : (watchMode || !production); +// 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`); + }); + } + }; +} + +const cwd = process.cwd(); + +// 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 + const isHidden = /^\./.test(fname); + if (ev !== "change" || isHidden) { + 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" }, + minify: production, + nodePaths, + outdir, + target: ['es2020'], + plugins: [ + cleanPlugin(), + ...lint + ? [ + stylelintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(css?|scss?)$') }), + eslintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(jsx?|js?)$') }) + ] + : [], + // 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 { + await context.rebuild(); +} catch (e) { + if (!watchMode) + process.exit(1); + // ignore errors in watch mode +} + +if (watchMode) { + 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/cockpituous-release b/cockpituous-release deleted file mode 100644 index b638fd7..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-session-recording.spec" -RELEASE_SRPM="_release/srpm" - -job release-source -job release-srpm -V - -# 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.session-recording.metainfo.xml b/org.cockpit-project.session-recording.metainfo.xml index 8281f0c..f819538 100644 --- a/org.cockpit-project.session-recording.metainfo.xml +++ b/org.cockpit-project.session-recording.metainfo.xml @@ -1,17 +1,18 @@ - org.cockpit-project.session-recording + org.cockpit_project.session-recording CC0-1.0 Session Recording - - Session Recording module for Cockpit - + Session Recording module for Cockpit

- Provides Session Recording module for Cockpit. Provides list of recorded by tlog terminal sessions from Journal. + Provides Session Recoridng moduel for Cockpit. Provides list of recorded by tlog terminal sessions from Journal. Allows to play them in a player with various controls. Shows correlated logs which happened during session.

org.cockpit_project.cockpit session-recording + https://github.com/Scribery/cockpit-session-recording + https://github.com/Scribery/cockpit-session-recording/issues + cockpit-devel_AT_lists.fedorahosted.org
diff --git a/package.json b/package.json index 4a0f4f5..6d9303e 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,51 @@ { "name": "session-recording", - "version": "0.1.0", "description": "Module for Cockpit which provides session recording configuration and playback", + "type": "module", "main": "index.js", "repository": "git@github.com:Scribery/cockpit-session-recording.git", "author": "", "license": "LGPL-2.1", "scripts": { - "watch": "webpack --watch", - "build": "webpack", + "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/" + "eslint:fix": "eslint --fix --ext .js --ext .jsx src/", + "stylelint": "stylelint src/*{.css,scss}", + "stylelint:fix": "stylelint --fix src/*{.css,scss}" }, "devDependencies": { - "@babel/core": "^7.20.12", - "@babel/eslint-parser": "^7.19.1", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.0.0", - "babel-loader": "^9.1.2", - "chrome-remote-interface": "^0.32.0", - "compression-webpack-plugin": "^10.0.0", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.3", - "eslint": "^8.31.0", - "eslint-config-standard": "^17.0.0", - "eslint-config-standard-react": "^9.2.0", + "argparse": "^2.0.1", + "chrome-remote-interface": "^0.32.1", + "esbuild": "^0.17.15", + "esbuild-plugin-copy": "^2.1.1", + "esbuild-plugin-replace": "^1.3.0", + "esbuild-sass-plugin": "^2.8.0", + "esbuild-wasm": "^0.17.16", + "eslint": "^8.13.0", + "eslint-config-react-app": "^7.0.0", + "eslint-config-standard": "^17.0.0-1", + "eslint-config-standard-jsx": "^11.0.0-1", "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.27.4", + "eslint-plugin-import": "^2.26.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.14.3", - "eslint-plugin-react-hooks": "^4.2.0", - "eslint-plugin-standard": "^4.1.0", - "eslint-webpack-plugin": "^3.2.0", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react-hooks": "^4.4.0", "htmlparser": "^1.7.7", "jed": "^1.1.1", - "mini-css-extract-plugin": "^2.7.2", "po2json": "^1.0.0-alpha", - "sass": "^1.57.1", - "sass-loader": "^13.2.0", - "sizzle": "^2.3.9", - "stdio": "^2.1.1", - "string-replace-loader": "^3.1.0", - "webpack": "^5.75.0", - "webpack-cli": "^5.0.1" + "qunit": "^2.9.3", + "sass": "^1.61.0", + "sizzle": "^2.3.3", + "stylelint": "^14.9.1", + "stylelint-config-standard-scss": "^5.0.0", + "stylelint-formatter-pretty": "^3.2.0" }, "dependencies": { - "@patternfly/patternfly": "4.132.2", - "@patternfly/react-core": "4.152.4", - "@patternfly/react-icons": "^4.19.8", - "@patternfly/react-table": "4.29.58", - "buffer": "^6.0.3", - "comment-json": "^4.2.3", - "core-js": "3.27.1", - "date-fns": "2.29.3", - "ini": "^3.0.1", - "jquery": "3.6.3", - "raw-loader": "^4.0.2", - "react": "16.13.1", - "react-dom": "16.13.1", - "throttle-debounce": "5.0.0", - "xterm": "^3.14.5" + "@patternfly/patternfly": "4.224.4", + "@patternfly/react-core": "4.276.9", + "react": "17.0.2", + "react-dom": "17.0.2" } } diff --git a/cockpit-session-recording.spec.in b/packaging/cockpit-session-recording.spec.in similarity index 89% rename from cockpit-session-recording.spec.in rename to packaging/cockpit-session-recording.spec.in index 5520ba7..82be084 100644 --- a/cockpit-session-recording.spec.in +++ b/packaging/cockpit-session-recording.spec.in @@ -7,26 +7,37 @@ URL: https://github.com/Scribery/%{name} Source: https://github.com/Scribery/%{name}/releases/download/%{version}/%{name}-%{version}.tar.xz BuildArch: noarch -BuildRequires: libappstream-glib +BuildRequires: nodejs BuildRequires: make +BuildRequires: libappstream-glib +BuildRequires: gettext +%if 0%{?rhel} && 0%{?rhel} <= 8 +BuildRequires: libappstream-glib-devel +%endif + Requires: cockpit-system Requires: tlog +%{NPM_PROVIDES} + %description Cockpit module providing session recording configuration and playback. This module allows viewing and playback of journal-stored terminal session recordings generated by the tlog component. %prep -%setup -qn cockpit-session-recording +%setup -q -n %{name} + +%build +# Nothing to build %install -%make_install +%make_install PREFIX=/usr appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/* %files -%{_datadir}/cockpit/session-recording -%{_datadir}/metainfo/org.cockpit-project.session-recording.metainfo.xml +%{_datadir}/cockpit/* +%{_datadir}/metainfo/* %changelog * Wed Jan 13 2021 Justin Stephenson - 7-1 diff --git a/po/de.po b/po/de.po index d117b0b..ed65a63 100644 --- a/po/de.po +++ b/po/de.po @@ -4,27 +4,20 @@ 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 -msgid "Starter Kit" -msgstr "Bausatz" - #: src/app.jsx:29 msgid "Unknown" msgstr "Unbekannt" 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 5376ef8..0000000 --- a/po/manifest2po +++ /dev/null @@ -1,185 +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_keywords(keywords) { - keywords.forEach(v => { - v.matches.forEach(keyword => - push({ - msgid: keyword, - locations: [ filename ] - }) - ); - }); -} - -function process_docs(docs) { - docs.forEach(doc => { - push({ - msgid: doc.label, - locations: [ filename ] - }) - }); -} - -function process_menu(menu) { - for (var m in menu) { - if (menu[m].label) { - push({ - msgid: menu[m].label, - locations: [ filename ] - }); - } - if (menu[m].keywords) - process_keywords(menu[m].keywords); - if (menu[m].docs) - process_docs(menu[m].docs); - } -} - -/* 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/src/app.jsx b/src/app.jsx index b94a4d2..35db035 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -19,7 +19,6 @@ import cockpit from 'cockpit'; import React from 'react'; -import './app.scss'; import View from "./recordings.jsx"; const _ = cockpit.gettext; @@ -29,10 +28,9 @@ export class Application extends React.Component { super(); this.state = { hostname: _("Unknown") }; - cockpit.file('/etc/hostname').read() - .done((content) => { - this.setState({ hostname: content.trim() }); - }); + cockpit.file('/etc/hostname').watch(content => { + this.setState({ hostname: content.trim() }); + }); } render() { diff --git a/src/index.html b/src/index.html index 166a62b..3ebce4a 100644 --- a/src/index.html +++ b/src/index.html @@ -1,7 +1,5 @@ - - + - Session Recording + Cockpit Session Recording - + - - - + + - -
+ +
- diff --git a/src/index.js b/src/index.js index b718ab6..ff46bb7 100644 --- a/src/index.js +++ b/src/index.js @@ -17,12 +17,11 @@ * along with Cockpit; If not, see . */ -import "./lib/patternfly/patternfly-4-cockpit.scss"; - -import "core-js/stable"; +import "cockpit-dark-theme"; +import "patternfly/patternfly-4-cockpit.scss"; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { Application } from './app.jsx'; /* * PF4 overrides need to come after the JSX components imports because @@ -31,8 +30,10 @@ import { Application } from './app.jsx'; * out of the dist/index.js and since it will maintain the order of the imported CSS, * the overrides will be correctly in the end of our stylesheet. */ -import "./lib/patternfly/patternfly-4-overrides.scss"; +import "patternfly/patternfly-4-overrides.scss"; +import './app.scss'; document.addEventListener("DOMContentLoaded", function () { - ReactDOM.render(React.createElement(Application, {}), document.getElementById('app')); + const root = createRoot(document.getElementById('app')); + root.render(); }); diff --git a/src/manifest.json b/src/manifest.json index 8dc3b2c..b52d873 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -3,7 +3,7 @@ "name": "session-recording", "requires": { - "cockpit": "122" + "cockpit": "137" }, "menu": { diff --git a/src/player.css b/src/player.css index 54cfe0f..975812d 100644 --- a/src/player.css +++ b/src/player.css @@ -67,16 +67,16 @@ .search-wrap { min-height: 25px; - display:block; - clear:both; + display: block; + clear: both; } .session_time { - margin-right:5px; + margin-right: 5px; } -.pf-c-progress__indicator:after { - content: ""; +.pf-c-progress__indicator::after { + content: ""; position: relative; width: 20px; height: 20px; diff --git a/src/player.jsx b/src/player.jsx index 8705e89..ddad79a 100644 --- a/src/player.jsx +++ b/src/player.jsx @@ -623,7 +623,7 @@ const PacketBuffer = class { function SearchEntry(props) { return ( - props.fastForwardToTS(props.pos, e)}>{formatDuration(props.pos)} + props.fastForwardToTS(props.pos, e)}>{formatDuration(props.pos)} ); } diff --git a/test/check-application b/test/check-application index 4f17277..54481fe 100755 --- a/test/check-application +++ b/test/check-application @@ -13,10 +13,12 @@ import configparser 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")) -from testlib import * +import testlib -# Test with pre-recorded journal files -class TestApplication(MachineCase): +# 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 _login(self, loc="/session-recording", wait="#app"): self.login_and_go(loc) b = self.browser @@ -408,4 +410,4 @@ class TestApplication(MachineCase): b.wait_present("#app") if __name__ == "__main__": - test_main() + testlib.test_main() diff --git a/test/reference-image b/test/reference-image new file mode 100644 index 0000000..9d9a2b2 --- /dev/null +++ b/test/reference-image @@ -0,0 +1 @@ +fedora-36 diff --git a/test/run b/test/run index ed43e3a..32a21df 100755 --- a/test/run +++ b/test/run @@ -4,5 +4,11 @@ set -eu # This is the expected entry point for Cockpit CI; will be called without # arguments but with an appropriate $TEST_OS, and optionally $TEST_SCENARIO -[ -z "${TEST_SCENARIO:-}" ] || export TEST_BROWSER="$TEST_SCENARIO" +TEST_SCENARIO="${TEST_SCENARIO:-}" +[ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox +export RUN_TESTS_OPTIONS=--track-naughties + +# linters are off by default for production builds, but we want to run them in CI +export LINT=1 + make check diff --git a/test/vm.install b/test/vm.install index 5241745..6bc7b8f 100644 --- a/test/vm.install +++ b/test/vm.install @@ -1,7 +1,7 @@ #!/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) printf "[WebService]\\nAllowUnencrypted=true\\n" > /etc/cockpit/cockpit.conf @@ -11,5 +11,5 @@ if type firewall-cmd >/dev/null 2>&1; then fi systemctl enable cockpit.socket -# HACK: See https://github.com/cockpit-project/cockpit/issues/14133 -mkdir -p /usr/share/cockpit/packagekit +# needed for testAppMenu +dnf install -y cockpit-packagekit diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index ecc0f0d..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,202 +0,0 @@ -const path = require("path"); -const copy = require("copy-webpack-plugin"); -const extract = require("mini-css-extract-plugin"); -const fs = require("fs"); -const webpack = require("webpack"); -const CompressionPlugin = require("compression-webpack-plugin"); -const ESLintPlugin = require('eslint-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 libdir = path.resolve(srcdir, "lib") -// absolute path disables recursive module resolution, so build a relative one -const nodedir = path.relative(process.cwd(), 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.js", - ], - }, - files: [ - "index.html", - "manifest.json" - ], -}; - -var output = { - path: distdir, - filename: "[name].js", - sourceMapFilename: "[file].map", -}; - -// Non-JS files which are copied verbatim to dist/ -const copy_files = [ - "./src/index.html", - "./src/manifest.json", -]; - -/* - * 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; - -const plugins = [ - new copy({ patterns: copy_files }), - new extract({filename: "[name].css"}), - new ESLintPlugin({ extensions: ["js", "jsx"], exclude: ["spec", "node_modules", "src/lib"] }), -]; - -/* Only minimize when in production mode */ -if (production) { - plugins.unshift( - new CompressionPlugin({ - test: /\.(css|js|html)$/, - deleteOriginalAssets: true, - })); -} - -var babel_loader = { - loader: "babel-loader", - options: { - presets: [ - [ - "@babel/env", - { - targets: { - chrome: "57", - firefox: "52", - safari: "10.3", - edge: "16", - opera: "44", - }, - }, - ], - "@babel/preset-react", - ], - }, -}; - -module.exports = { - mode: production ? "production" : "development", - resolve: { - modules: [libdir, nodedir], - }, - entry: info.entries, - externals: externals, - output: output, - devtool: "source-map", - module: { - rules: [ - { - exclude: /node_modules/, - use: babel_loader, - test: /\.(js|jsx)$/, - }, - /* HACK: remove unwanted fonts from PatternFly's css */ - { - test: /patternfly-4-cockpit.scss$/, - use: [ - extract.loader, - { - loader: "css-loader", - options: { - sourceMap: true, - url: false, - }, - }, - { - loader: "string-replace-loader", - options: { - multiple: [ - { - search: /src:url\("patternfly-icons-fake-path\/pficon[^}]*/g, - replace: "src:url('fonts/patternfly.woff')format('woff');", - }, - { - search: /@font-face[^}]*patternfly-fonts-fake-path[^}]*}/g, - replace: "", - }, - ], - }, - }, - { - loader: 'sass-loader', - options: { - sourceMap: !production, - sassOptions: { - outputStyle: production ? 'compressed' : undefined, - }, - }, - }, - ], - }, - { - test: /\.s?css$/, - exclude: /patternfly-4-cockpit.scss/, - use: [ - extract.loader, - { - loader: "css-loader", - options: { - sourceMap: true, - url: false, - }, - }, - { - loader: 'sass-loader', - options: { - sourceMap: !production, - sassOptions: { - outputStyle: production ? 'compressed' : undefined, - }, - }, - }, - ], - }, - ], - }, - plugins: plugins, -};