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, -};