Compare commits
208 commits
main
...
drop-po2js
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dce9b02e3 | ||
|
|
5cb865dc07 | ||
|
|
c6912fc484 | ||
|
|
4128c04f47 | ||
|
|
9d5445a99e | ||
|
|
d9b8fb2b13 | ||
|
|
142d42066b | ||
|
|
915521a6ad | ||
|
|
d7eb158f79 | ||
|
|
b3c40d9a4d | ||
|
|
ec2325a3db | ||
|
|
27ec379aad | ||
|
|
9026069b3c | ||
|
|
f05760e535 | ||
|
|
cba733642f | ||
|
|
8f9d7bb636 | ||
|
|
a4a89eab52 | ||
|
|
9f5d0d42b7 | ||
|
|
ef6634a0c7 | ||
|
|
73ac17ce03 | ||
|
|
cba99b097a | ||
|
|
a89d6d646c | ||
|
|
e9f6c15d70 | ||
|
|
7623d95e11 | ||
|
|
bd2765f636 | ||
|
|
523358b03d | ||
|
|
3e29263d45 | ||
|
|
e0c8d9769b | ||
|
|
804a15b07f | ||
|
|
8564be5f8f | ||
|
|
ae3d2b77cb | ||
|
|
0622a5e06a | ||
|
|
4426f62000 | ||
|
|
9fe6a8229a | ||
|
|
efba93ce98 | ||
|
|
12ab19bb67 | ||
|
|
4b0bfc7648 | ||
|
|
7c82b32a59 | ||
|
|
81a3650eba | ||
|
|
4dbf79bcef | ||
|
|
0470867bd3 | ||
|
|
3232b641f6 | ||
|
|
6b7d8134f0 | ||
|
|
b2738e6548 | ||
|
|
4367e8dd61 | ||
|
|
0ae9d17487 | ||
|
|
acf2d299ac | ||
|
|
cfd219f31f | ||
|
|
2336ba0e91 | ||
|
|
d589513534 | ||
|
|
7414584afe | ||
|
|
806eeab1f2 | ||
|
|
5b5fc11b94 | ||
|
|
ce70a6d4ee | ||
|
|
c3616baaa2 | ||
|
|
a378c5dbf3 | ||
|
|
f577208220 | ||
|
|
86f674bd92 | ||
|
|
07b5b13b12 | ||
|
|
aeb3cb6d8d | ||
|
|
e151c9ee8f | ||
|
|
7c75596330 | ||
|
|
e9490a1a10 | ||
|
|
cc2a205b13 | ||
|
|
527941da25 | ||
|
|
0089d35bef | ||
|
|
b73f42eb38 | ||
|
|
d41cf3bfcc | ||
|
|
235f110ec7 | ||
|
|
a0fffde59d | ||
|
|
38d4b00533 | ||
|
|
d22ac8ef1a | ||
|
|
e2856e3160 | ||
|
|
8151aa2d49 | ||
|
|
02c5b475c6 | ||
|
|
4f44ec3e22 | ||
|
|
95723f2817 | ||
|
|
9e482dab7d | ||
|
|
92f3b7b75d | ||
|
|
39a359bffb | ||
|
|
6f5ec24e16 | ||
|
|
88a167a89a | ||
|
|
e716567dfc | ||
|
|
bab09074b3 | ||
|
|
da32f4f344 | ||
|
|
1d81c8e828 | ||
|
|
c855f12deb | ||
|
|
fec5fd1ae7 | ||
|
|
3244a4c37d | ||
|
|
3ebdcfa4fe | ||
|
|
47c1eb0804 | ||
|
|
8b9f7f490e | ||
|
|
03a5445b35 | ||
|
|
abc5922946 | ||
|
|
4be61896ea | ||
|
|
e13c9d1bf3 | ||
|
|
e49176b966 | ||
|
|
7ac04a4917 | ||
|
|
c48e7f69d1 | ||
|
|
2fe72717b2 | ||
|
|
d3110ea7ef | ||
|
|
7092e13518 | ||
|
|
d2708cf533 | ||
|
|
f7b2bdccda | ||
|
|
d038d2bd55 | ||
|
|
6379950582 | ||
|
|
7ff310a705 | ||
|
|
2c16ca3e2f | ||
|
|
e561f18f56 | ||
|
|
370f58c543 | ||
|
|
255a8bdde1 | ||
|
|
fcfc5f40f8 | ||
|
|
74bc71b190 | ||
|
|
e504489ab0 | ||
|
|
819da4d495 | ||
|
|
0e8f87a000 | ||
|
|
ada0bacaed | ||
|
|
cf618957af | ||
|
|
aa63c3871c | ||
|
|
46ad9834b3 | ||
|
|
7eada9f82a | ||
|
|
bc24785d98 | ||
|
|
7d9391da3f | ||
|
|
0a85319c5e | ||
|
|
36173c8b9c | ||
|
|
8275cca551 | ||
|
|
11fd640fe5 | ||
|
|
7592ce8ab0 | ||
|
|
fe02babb2f | ||
|
|
95c92fd984 | ||
|
|
c4d2ec525b | ||
|
|
de0b26f1e4 | ||
|
|
5283e234a1 | ||
|
|
019f61fda1 | ||
|
|
78c850acf3 | ||
|
|
179fb8c5e6 | ||
|
|
4abce7ae8d | ||
|
|
395cbdc2c9 | ||
|
|
6a7f6805d9 | ||
|
|
5a6e0beb53 | ||
|
|
198e49cfff | ||
|
|
564c9c25f7 | ||
|
|
ca14ae94ec | ||
|
|
4bdbec47fb | ||
|
|
bd2e75f7ee | ||
|
|
adc5913b76 | ||
|
|
7032a2e74f | ||
|
|
d2a9be4564 | ||
|
|
f2e5bc2903 | ||
|
|
046a1d4cb1 | ||
|
|
45c8d762a2 | ||
|
|
e02df0759c | ||
|
|
7c15858444 | ||
|
|
feed483646 | ||
|
|
811b80fa27 | ||
|
|
5348d111c4 | ||
|
|
ac612470bd | ||
|
|
849fcd2d49 | ||
|
|
228645236a | ||
|
|
cddcb1f40a | ||
|
|
229671f485 | ||
|
|
a77896f1c2 | ||
|
|
3d308cd75d | ||
|
|
fa691ce201 | ||
|
|
142dd4fb6a | ||
|
|
e904113eff | ||
|
|
ffeec0bd36 | ||
|
|
66998cafa8 | ||
|
|
09039778c2 | ||
|
|
6f6c6b7714 | ||
|
|
0ad6b11ebf | ||
|
|
5d51c45aa5 | ||
|
|
5705467b85 | ||
|
|
aadc75afd0 | ||
|
|
fc7790f08b | ||
|
|
776c4da012 | ||
|
|
1b32b0c0ce | ||
|
|
fff3a73253 | ||
|
|
20138d3e83 | ||
|
|
91877b0570 | ||
|
|
2162092977 | ||
|
|
67716138d2 | ||
|
|
0f37e525d7 | ||
|
|
2fe77ec1da | ||
|
|
75e3f0f4d3 | ||
|
|
e2a6b5ee81 | ||
|
|
5f58af7624 | ||
|
|
1feb78fab7 | ||
|
|
f69b9c1887 | ||
|
|
460b044720 | ||
|
|
c51610e22e | ||
|
|
adc8159def | ||
|
|
300a896483 | ||
|
|
39778968b3 | ||
|
|
2118f9e212 | ||
|
|
0a593d20d0 | ||
|
|
b59140d328 | ||
|
|
2aa3270d97 | ||
|
|
1abe64fe0c | ||
|
|
4ca9b76b23 | ||
|
|
0ce08a4420 | ||
|
|
0852de4222 | ||
|
|
b7c21ae104 | ||
|
|
ea3eb80c07 | ||
|
|
b1a44e337a | ||
|
|
9c605da2f6 | ||
|
|
471d2c160b | ||
|
|
a20e3c5a81 |
46 changed files with 5160 additions and 1082 deletions
4
.babelrc
4
.babelrc
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"presets": ["@babel/env",
|
||||
"@babel/preset-react"]
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
node_modules/*
|
||||
pkg/lib/*
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "standard", "standard-react"],
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["eslint:recommended", "standard", "standard-jsx", "standard-react"],
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "2022",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["flowtype", "react"],
|
||||
"plugins": ["flowtype", "react", "react-hooks"],
|
||||
"rules": {
|
||||
"indent": ["error", 4,
|
||||
{
|
||||
|
|
@ -22,11 +19,15 @@
|
|||
"ignoredNodes": [ "JSXAttribute" ]
|
||||
}],
|
||||
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
|
||||
"no-var": "error",
|
||||
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
|
||||
"prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }],
|
||||
"react/jsx-indent": ["error", 4],
|
||||
"semi": ["error", "always", { "omitLastInOneLineBlock": true }],
|
||||
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
|
||||
"camelcase": "off",
|
||||
"comma-dangle": "off",
|
||||
"curly": "off",
|
||||
|
|
@ -38,12 +39,7 @@
|
|||
"react/jsx-indent-props": "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"
|
||||
"standard/no-callback-literal": "off"
|
||||
},
|
||||
"globals": {
|
||||
"require": false,
|
||||
|
|
|
|||
1
.fmf/version
Normal file
1
.fmf/version
Normal file
|
|
@ -0,0 +1 @@
|
|||
1
|
||||
37
.github/workflows/release.yml
vendored
Normal file
37
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Create a GitHub upstream release
|
||||
# See README.md.
|
||||
name: release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# this is a glob, not a regexp
|
||||
- '[0-9]*'
|
||||
jobs:
|
||||
source:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/cockpit-project/unit-tests
|
||||
options: --user root
|
||||
permissions:
|
||||
# create GitHub release
|
||||
contents: write
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
|
||||
- name: Pacify git's permission check
|
||||
run: git config --global --add safe.directory /__w/cockpit-session-recording/cockpit-session-recording
|
||||
|
||||
- name: Workaround for https://github.com/actions/checkout/pull/697
|
||||
run: git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags)
|
||||
|
||||
- name: Build release
|
||||
run: make dist
|
||||
|
||||
- name: Publish GitHub release
|
||||
uses: cockpit-project/action-release@88d994da62d1451c7073e26748c18413fcdf46e9
|
||||
with:
|
||||
filename: "cockpit-session-recording-${{ github.ref_name }}.tar.xz"
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
|||
*~
|
||||
*.retry
|
||||
*.tar.gz
|
||||
*.tar.xz
|
||||
*.rpm
|
||||
node_modules/
|
||||
dist/
|
||||
|
|
@ -8,8 +8,12 @@ dist/
|
|||
/.vagrant
|
||||
package-lock.json
|
||||
Test*FAIL*
|
||||
bots/
|
||||
/bots
|
||||
test/common/
|
||||
test/images/
|
||||
pkg
|
||||
*.pot
|
||||
POTFILES*
|
||||
tmp/
|
||||
/po/LINGUAS
|
||||
/tools
|
||||
|
|
|
|||
38
.stylelintrc.json
Normal file
38
.stylelintrc.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
dist: trusty
|
||||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- "8"
|
||||
script:
|
||||
- npm install
|
||||
- npm run build
|
||||
208
Makefile
208
Makefile
|
|
@ -1,17 +1,45 @@
|
|||
# extract name from package.json
|
||||
PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' package.json)
|
||||
RPM_NAME := cockpit-$(PACKAGE_NAME)
|
||||
VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.')
|
||||
ifeq ($(TEST_OS),)
|
||||
TEST_OS = centos-7
|
||||
TEST_OS = centos-8-stream
|
||||
endif
|
||||
export TEST_OS
|
||||
TARFILE=$(RPM_NAME)-$(VERSION).tar.xz
|
||||
NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz
|
||||
SPEC=$(RPM_NAME).spec
|
||||
PREFIX ?= /usr/local
|
||||
APPSTREAMFILE=org.cockpit-project.$(PACKAGE_NAME).metainfo.xml
|
||||
VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS)
|
||||
# one example directory from `npm install` to check if that already ran
|
||||
NODE_MODULES_TEST=node_modules/po2json
|
||||
# one example file in dist/ from webpack to check if that already ran
|
||||
WEBPACK_TEST=dist/index.html
|
||||
# stamp file to check for node_modules/
|
||||
NODE_MODULES_TEST=package-lock.json
|
||||
# one example file in dist/ from bundler to check if that already ran
|
||||
DIST_TEST=dist/manifest.json
|
||||
# one example file in pkg/lib to check if it was already checked out
|
||||
COCKPIT_REPO_STAMP=pkg/lib/cockpit-po-plugin.js
|
||||
# common arguments for tar, mostly to make the generated tarballs reproducible
|
||||
TAR_ARGS = --sort=name --mtime "@$(shell git show --no-patch --format='%at')" --mode=go=rX,u+rw,a-s --numeric-owner --owner=0 --group=0
|
||||
|
||||
all: $(WEBPACK_TEST)
|
||||
all: $(DIST_TEST)
|
||||
|
||||
# checkout common files from Cockpit repository required to build this project;
|
||||
# this has no API stability guarantee, so check out a stable tag when you start
|
||||
# a new project, use the latest release, and update it from time to time
|
||||
COCKPIT_REPO_FILES = \
|
||||
pkg/lib \
|
||||
test/common \
|
||||
$(NULL)
|
||||
|
||||
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
|
||||
COCKPIT_REPO_COMMIT = 9c73bec7e1dc2395a00aa0c510fd7210b6c96a16 # 300.1 + 42 commits
|
||||
|
||||
$(COCKPIT_REPO_FILES): $(COCKPIT_REPO_STAMP)
|
||||
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
||||
$(COCKPIT_REPO_STAMP): Makefile
|
||||
@git rev-list --quiet --objects $(COCKPIT_REPO_TREE) -- 2>/dev/null || \
|
||||
git fetch --no-tags --no-write-fetch-head --depth=1 $(COCKPIT_REPO_URL) $(COCKPIT_REPO_COMMIT)
|
||||
git archive $(COCKPIT_REPO_TREE) -- $(COCKPIT_REPO_FILES) | tar x
|
||||
|
||||
#
|
||||
# i18n
|
||||
|
|
@ -19,89 +47,98 @@ all: $(WEBPACK_TEST)
|
|||
|
||||
LINGUAS=$(basename $(notdir $(wildcard po/*.po)))
|
||||
|
||||
po/POTFILES.js.in:
|
||||
mkdir -p $(dir $@)
|
||||
find src/ -name '*.js' -o -name '*.jsx' -o -name '*.es6' > $@
|
||||
|
||||
po/$(PACKAGE_NAME).js.pot: po/POTFILES.js.in
|
||||
xgettext --default-domain=cockpit --output=$@ --language=C --keyword= \
|
||||
--keyword=_:1,1t --keyword=_:1c,2,1t --keyword=C_:1c,2 \
|
||||
po/$(PACKAGE_NAME).js.pot:
|
||||
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ --language=C --keyword= \
|
||||
--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) $(shell find src/ -type f) package.json webpack.config.js $(patsubst %,dist/po.%.js,$(LINGUAS))
|
||||
NODE_ENV=$(NODE_ENV) npm run build
|
||||
$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js
|
||||
NODE_ENV=$(NODE_ENV) ./build.js
|
||||
|
||||
watch: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
|
||||
NODE_ENV=$(NODE_ENV) ./build.js --watch
|
||||
|
||||
clean:
|
||||
rm -rf dist/
|
||||
[ ! -e cockpit-$(PACKAGE_NAME).spec.in ] || rm -f cockpit-$(PACKAGE_NAME).spec
|
||||
rm -f $(SPEC)
|
||||
rm -f po/LINGUAS
|
||||
|
||||
install: $(WEBPACK_TEST)
|
||||
mkdir -p $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME)
|
||||
cp -r dist/* $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME)
|
||||
mkdir -p $(DESTDIR)/usr/share/metainfo/
|
||||
cp org.cockpit-project.$(PACKAGE_NAME).metainfo.xml $(DESTDIR)/usr/share/metainfo/
|
||||
install: $(DIST_TEST) po/LINGUAS
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME)
|
||||
cp -r dist/* $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME)
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/share/metainfo/
|
||||
msgfmt --xml -d po \
|
||||
--template $(APPSTREAMFILE) \
|
||||
-o $(DESTDIR)$(PREFIX)/share/metainfo/$(APPSTREAMFILE)
|
||||
|
||||
# this requires a built source tree and avoids having to install anything system-wide
|
||||
devel-install: $(WEBPACK_TEST)
|
||||
devel-install: $(DIST_TEST)
|
||||
mkdir -p ~/.local/share/cockpit
|
||||
ln -s `pwd`/dist ~/.local/share/cockpit/$(PACKAGE_NAME)
|
||||
|
||||
# when building a distribution tarball, call webpack with a 'production' environment
|
||||
# ship a stub node_modules/ so that `make` works without re-running `npm install`
|
||||
dist-gzip: NODE_ENV=production
|
||||
dist-gzip: all cockpit-$(PACKAGE_NAME).spec
|
||||
mv node_modules node_modules.release
|
||||
mkdir -p $(NODE_MODULES_TEST)
|
||||
touch -r package.json $(NODE_MODULES_TEST)
|
||||
touch dist/*
|
||||
tar czf cockpit-$(PACKAGE_NAME)-$(VERSION).tar.gz --transform 's,^,cockpit-$(PACKAGE_NAME)/,' \
|
||||
--exclude cockpit-$(PACKAGE_NAME).spec.in \
|
||||
$$(git ls-files) cockpit-$(PACKAGE_NAME).spec dist/ node_modules
|
||||
rm -rf node_modules
|
||||
mv node_modules.release node_modules
|
||||
# assumes that there was symlink set up using the above devel-install target,
|
||||
# and removes it
|
||||
devel-uninstall:
|
||||
rm -f ~/.local/share/cockpit/$(PACKAGE_NAME)
|
||||
|
||||
srpm: dist-gzip cockpit-$(PACKAGE_NAME).spec
|
||||
print-version:
|
||||
@echo "$(VERSION)"
|
||||
|
||||
dist: $(TARFILE)
|
||||
@ls -1 $(TARFILE)
|
||||
|
||||
# when building a distribution tarball, call bundler with a 'production' environment
|
||||
# we don't ship node_modules for license and compactness reasons; we ship a
|
||||
# pre-built dist/ (so it's not necessary) and ship package-lock.json (so that
|
||||
# node_modules/ can be reconstructed if necessary)
|
||||
$(TARFILE): export NODE_ENV=production
|
||||
$(TARFILE): $(DIST_TEST) $(SPEC)
|
||||
if type appstream-util >/dev/null 2>&1; then appstream-util validate-relax --nonet *.metainfo.xml; fi
|
||||
tar --xz $(TAR_ARGS) -cf $(TARFILE) --transform 's,^,$(RPM_NAME)/,' \
|
||||
--exclude packaging/$(SPEC).in --exclude node_modules \
|
||||
$$(git ls-files) $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) $(SPEC) dist/
|
||||
|
||||
$(NODE_CACHE): $(NODE_MODULES_TEST)
|
||||
tar --xz $(TAR_ARGS) -cf $@ node_modules
|
||||
|
||||
node-cache: $(NODE_CACHE)
|
||||
|
||||
# convenience target for developers
|
||||
srpm: $(TARFILE) $(NODE_CACHE) $(SPEC)
|
||||
rpmbuild -bs \
|
||||
--define "_sourcedir `pwd`" \
|
||||
--define "_srcrpmdir `pwd`" \
|
||||
cockpit-$(PACKAGE_NAME).spec
|
||||
$(SPEC)
|
||||
|
||||
rpm: dist-gzip cockpit-$(PACKAGE_NAME).spec
|
||||
# convenience target for developers
|
||||
rpm: $(TARFILE) $(NODE_CACHE) $(SPEC)
|
||||
mkdir -p "`pwd`/output"
|
||||
mkdir -p "`pwd`/rpmbuild"
|
||||
rpmbuild -bb \
|
||||
|
|
@ -111,39 +148,46 @@ rpm: dist-gzip cockpit-$(PACKAGE_NAME).spec
|
|||
--define "_srcrpmdir `pwd`" \
|
||||
--define "_rpmdir `pwd`/output" \
|
||||
--define "_buildrootdir `pwd`/build" \
|
||||
cockpit-$(PACKAGE_NAME).spec
|
||||
$(SPEC)
|
||||
find `pwd`/output -name '*.rpm' -printf '%f\n' -exec mv {} . \;
|
||||
rm -r "`pwd`/rpmbuild"
|
||||
rm -r "`pwd`/output" "`pwd`/build"
|
||||
|
||||
# build a VM with locally built rpm installed
|
||||
$(VM_IMAGE): rpm bots
|
||||
rm -f $(VM_IMAGE) $(VM_IMAGE).qcow2
|
||||
bots/image-customize -v -i cockpit -i `pwd`/cockpit-$(PACKAGE_NAME)-*.noarch.rpm -s $(CURDIR)/test/vm.install $(TEST_OS)
|
||||
# build a VM with locally built distro pkgs installed
|
||||
# disable networking, VM images have mock/pbuilder with the common build dependencies pre-installed
|
||||
$(VM_IMAGE): $(TARFILE) $(NODE_CACHE) bots test/vm.install
|
||||
bots/image-customize --fresh \
|
||||
--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)
|
||||
|
||||
# run the browser integration tests; skip check for SELinux denials
|
||||
check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common
|
||||
TEST_AUDIT_NO_SELINUX=1 test/check-application
|
||||
# convenience target to print the filename of the test image
|
||||
print-vm:
|
||||
@echo $(VM_IMAGE)
|
||||
|
||||
# checkout Cockpit's bots/ directory for standard test VM images and API to launch them
|
||||
# must be from cockpit's master, as only that has current and existing images; but testvm.py API is stable
|
||||
bots:
|
||||
git fetch --depth=1 https://github.com/cockpit-project/cockpit.git
|
||||
git checkout --force FETCH_HEAD -- bots/
|
||||
git reset bots
|
||||
# convenience target to setup all the bits needed for the integration tests
|
||||
# without actually running them
|
||||
prepare-check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common
|
||||
|
||||
# checkout Cockpit's test API; this has no API stability guarantee, so check out a stable tag
|
||||
# when you start a new project, use the latest relese, and update it from time to time
|
||||
test/common:
|
||||
git fetch --depth=1 https://github.com/cockpit-project/cockpit.git 176
|
||||
git checkout --force FETCH_HEAD -- test/common
|
||||
git reset test/common
|
||||
# run the browser integration tests
|
||||
# this will run all tests/check-* and format them as TAP
|
||||
check: prepare-check
|
||||
test/common/run-tests ${RUN_TESTS_OPTIONS}
|
||||
|
||||
# checkout Cockpit's bots for standard test VM images and API to launch them
|
||||
bots: $(COCKPIT_REPO_STAMP)
|
||||
test/common/make-bots
|
||||
|
||||
$(NODE_MODULES_TEST): package.json
|
||||
npm install
|
||||
# if it exists already, npm install won't update it; force that so that we always get up-to-date packages
|
||||
rm -f package-lock.json
|
||||
# unset NODE_ENV, skips devDependencies otherwise
|
||||
env -u NODE_ENV npm install --ignore-scripts
|
||||
env -u NODE_ENV npm prune
|
||||
|
||||
.PHONY: all clean install devel-install dist-gzip srpm rpm check vm update-po
|
||||
.PHONY: all clean install devel-install devel-uninstall print-version dist node-cache rpm prepare-check check vm print-vm
|
||||
|
|
|
|||
142
README.md
142
README.md
|
|
@ -1,6 +1,24 @@
|
|||
# Cockpit Starter Kit
|
||||
# Cockpit Session Recording
|
||||
|
||||
Scaffolding for a [Cockpit](http://www.cockpit-project.org) module.
|
||||
Module for [Cockpit](http://www.cockpit-project.org) which provides session recording
|
||||
configuration and playback.
|
||||
It requires [tlog](https://github.com/Scribery/tlog) to record terminal sessions.
|
||||
SSSD is required to manage which users / groups are recorded. Systemd Journal is used to store recordings.
|
||||
Ansible role for session-recording is [here](https://github.com/nkinder/session-recording).
|
||||
|
||||
Demos & Talks:
|
||||
|
||||
* [Demo 1 on YouTube](https://youtu.be/5-0WBf4rOrc)
|
||||
* [Demo 2 on YouTube](https://youtu.be/Fw8g_fFvwcs)
|
||||
* [FOSDEM talk](https://youtu.be/sHO5y28EHXg)
|
||||
|
||||
GitHub Organization:
|
||||
|
||||
* [scribery.github.io](http://scribery.github.io/)
|
||||
* [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
|
||||
|
||||
|
|
@ -8,37 +26,66 @@ Make sure you have `npm` available (usually from your distribution package).
|
|||
These commands check out the source and build it into the `dist/` directory:
|
||||
|
||||
```
|
||||
git clone https://github.com/cockpit-project/starter-kit.git
|
||||
cd starter-kit
|
||||
git clone https://github.com/Scribery/cockpit-session-recording.git
|
||||
cd cockpit-session-recording
|
||||
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
|
||||
ln -s `pwd`/dist ~/.local/share/cockpit/starter-kit
|
||||
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
|
||||
|
||||
$ ./build.js -w
|
||||
|
||||
or
|
||||
|
||||
$ make watch
|
||||
|
||||
When developing against a virtual machine, watch mode can also automatically upload
|
||||
the code changes by setting the `RSYNC` environment variable to
|
||||
the remote hostname.
|
||||
|
||||
$ RSYNC=c make watch
|
||||
|
||||
When developing against a remote host as a normal user, `RSYNC_DEVEL` can be
|
||||
set to upload code changes to `~/.local/share/cockpit/` instead of
|
||||
`/usr/local`.
|
||||
|
||||
$ RSYNC_DEVEL=example.com make watch
|
||||
|
||||
To "uninstall" the locally installed version, run `make devel-uninstall`, or
|
||||
remove manually the symlink:
|
||||
|
||||
rm ~/.local/share/cockpit/starter-kit
|
||||
|
||||
# Running eslint
|
||||
|
||||
Cockpit Starter Kit uses [ESLint](https://eslint.org/) to automatically check
|
||||
JavaScript code style in `.jsx` and `.es6` files.
|
||||
JavaScript 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:
|
||||
|
||||
|
|
@ -50,60 +97,49 @@ Violations of some rules can be fixed automatically by:
|
|||
|
||||
Rules configuration can be found in the `.eslintrc.json` file.
|
||||
|
||||
# Automated Testing
|
||||
## Running stylelint
|
||||
|
||||
Cockpit uses [Stylelint](https://stylelint.io/) to automatically check CSS code
|
||||
style in `.css` and `scss` files.
|
||||
|
||||
styleint is executed 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:
|
||||
|
||||
$ ./build.js -es
|
||||
|
||||
# Running tests locally
|
||||
|
||||
Run `make check` to build an RPM, install it into a standard Cockpit test VM
|
||||
(centos-7 by default), and run the test/check-application integration test on
|
||||
(centos-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 master (see the `test/common`
|
||||
Cockpit's test/common from a tag instead of main (see the `test/common`
|
||||
target in `Makefile`).
|
||||
|
||||
After the test VM is prepared, you can manually run the test without rebuilding
|
||||
the VM, possibly with extra options for tracing and halting on test failures
|
||||
(for interactive debugging):
|
||||
|
||||
TEST_OS=centos-7 test/check-application -tvs
|
||||
TEST_OS=centos-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-28 make check
|
||||
|
||||
# Vagrant
|
||||
|
||||
This directory contains a Vagrantfile that installs and starts cockpit on a
|
||||
Fedora 26 cloud image. Run `vagrant up` to start it and `vagrant rsync` to
|
||||
synchronize the `dist` directory to `/usr/local/share/cockit/starter-kit`. Use
|
||||
`vagrant rsync-auto` to automatically sync when contents of the `dist`
|
||||
directory change.
|
||||
|
||||
# Customizing
|
||||
|
||||
After cloning the Starter Kit you should rename the files, package names, and
|
||||
labels to your own project's name. Use these commands to find out what to
|
||||
change:
|
||||
|
||||
find -iname '*starter*'
|
||||
git grep -i starter
|
||||
|
||||
# Automated release
|
||||
|
||||
Once your cloned project is ready for a release, you should consider automating
|
||||
that. [Cockpituous release](https://github.com/cockpit-project/cockpituous/tree/master/release)
|
||||
aims to fully automate project releases to GitHub, Fedora, Ubuntu, COPR, Docker
|
||||
Hub, and other places. The intention is that the only manual step for releasing
|
||||
a project is to create a signed tag for the version number; pushing the tag
|
||||
then triggers a GitHub webhook that calls a set of release scripts (on
|
||||
Cockpit's CI infrastructure).
|
||||
|
||||
starter-kit includes an example [cockpitous release script](./cockpituous-release)
|
||||
that builds an upstream release tarball and source RPM. Please see the above
|
||||
cockpituous documentation for details.
|
||||
|
||||
# Further reading
|
||||
|
||||
* The [Starter Kit announcement](http://cockpit-project.org/blog/cockpit-starter-kit.html)
|
||||
blog post explains the rationale for this project.
|
||||
* [Cockpit Deployment and Developer documentation](http://cockpit-project.org/guide/latest/)
|
||||
* [Make your project easily discoverable](http://cockpit-project.org/blog/making-a-cockpit-application.html)
|
||||
TEST_OS=fedora-34 make check
|
||||
|
|
|
|||
30
Vagrantfile
vendored
30
Vagrantfile
vendored
|
|
@ -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
|
||||
140
build.js
Executable file
140
build.js
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
#!/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 production = process.env.NODE_ENV === 'production';
|
||||
const useWasm = os.arch() !== 'x64';
|
||||
const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default;
|
||||
const lintDefault = process.env.LINT ? process.env.LINT === '0' : production;
|
||||
|
||||
const parser = (await import('argparse')).default.ArgumentParser();
|
||||
parser.add_argument('-r', '--rsync', { help: "rsync bundles to ssh target after build", metavar: "HOST" });
|
||||
parser.add_argument('-w', '--watch', { action: 'store_true', help: "Enable watch mode", default: process.env.ESBUILD_WATCH === "true" });
|
||||
parser.add_argument('-e', '--no-eslint', { action: 'store_true', help: "Disable eslint linting", default: lintDefault });
|
||||
parser.add_argument('-s', '--no-stylelint', { action: 'store_true', help: "Disable stylelint linting", default: lintDefault });
|
||||
const args = parser.parse_args();
|
||||
|
||||
if (args.rsync)
|
||||
process.env.RSYNC = args.rsync;
|
||||
|
||||
// List of directories to use when using import statements
|
||||
const nodePaths = ['pkg/lib'];
|
||||
const outdir = 'dist';
|
||||
|
||||
// Obtain package name from package.json
|
||||
const packageJson = JSON.parse(fs.readFileSync('package.json'));
|
||||
|
||||
function notifyEndPlugin() {
|
||||
return {
|
||||
name: 'notify-end',
|
||||
setup(build) {
|
||||
let startTime;
|
||||
|
||||
build.onStart(() => {
|
||||
startTime = new Date();
|
||||
});
|
||||
|
||||
build.onEnd(() => {
|
||||
const endTime = new Date();
|
||||
const timeStamp = endTime.toTimeString().split(' ')[0];
|
||||
console.log(`${timeStamp}: Build finished in ${endTime - startTime} ms`);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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(),
|
||||
...args.no_stylelint ? [] : [stylelintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(css?|scss?)$') })],
|
||||
...args.no_eslint ? [] : [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 (!args.watch)
|
||||
process.exit(1);
|
||||
// ignore errors in watch mode
|
||||
}
|
||||
|
||||
if (args.watch) {
|
||||
const on_change = async path => {
|
||||
console.log("change detected:", path);
|
||||
await context.cancel();
|
||||
|
||||
try {
|
||||
await context.rebuild();
|
||||
} catch (e) {} // ignore in watch mode
|
||||
};
|
||||
|
||||
watch_dirs('src', on_change);
|
||||
|
||||
// wait forever until Control-C
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
context.dispose();
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
Name: cockpit-starter-kit
|
||||
Version: @VERSION@
|
||||
Release: 1%{?dist}
|
||||
Summary: Cockpit Starter Kit Example Module
|
||||
License: LGPLv2+
|
||||
|
||||
Source: cockpit-starter-kit-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
%define debug_package %{nil}
|
||||
|
||||
%description
|
||||
Cockpit Starter Kit Example Module
|
||||
|
||||
%prep
|
||||
%setup -n cockpit-starter-kit
|
||||
|
||||
%install
|
||||
%make_install
|
||||
|
||||
%files
|
||||
%{_datadir}/cockpit/*
|
||||
%{_datadir}/metainfo/*
|
||||
|
||||
%changelog
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# This is a script run to release welder-web through Cockpituous:
|
||||
# https://github.com/cockpit-project/cockpituous/tree/master/release
|
||||
|
||||
# Anything that start with 'job' may run in a way that it SIGSTOP's
|
||||
# itself when preliminary preparition and then gets a SIGCONT in
|
||||
# order to complete its work.
|
||||
#
|
||||
# Check cockpituous documentation for available release targets.
|
||||
|
||||
RELEASE_SOURCE="_release/source"
|
||||
RELEASE_SPEC="cockpit-starter-kit.spec"
|
||||
RELEASE_SRPM="_release/srpm"
|
||||
|
||||
job release-source
|
||||
job release-srpm
|
||||
|
||||
# Once you have a Fedora package and add the https://pagure.io/user/cockpit
|
||||
# user to your project's maintainers, you can also upload to Fedora automatically:
|
||||
|
||||
## Authenticate for pushing into Fedora dist-git (works in Cockpituous release container)
|
||||
# cat ~/.fedora-password | kinit cockpit@FEDORAPROJECT.ORG
|
||||
## Do fedora builds for the tag, using tarball
|
||||
# job release-koji -k master
|
||||
# job release-koji f29
|
||||
# job release-bodhi F29
|
||||
|
||||
# These are likely the first of your release targets; but run them after Fedora uploads,
|
||||
# so that failures there will fail the release early, before publishing on GitHub
|
||||
|
||||
# job release-github
|
||||
# job release-copr @myorg/myrepo
|
||||
18
org.cockpit-project.session-recording.metainfo.xml
Normal file
18
org.cockpit-project.session-recording.metainfo.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="addon">
|
||||
<id>org.cockpit_project.session-recording</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<name>Session Recording</name>
|
||||
<summary>Session Recording module for Cockpit</summary>
|
||||
<description>
|
||||
<p>
|
||||
Provides Session Recording module 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.
|
||||
</p>
|
||||
</description>
|
||||
<extends>org.cockpit_project.cockpit</extends>
|
||||
<launchable type="cockpit-manifest">session-recording</launchable>
|
||||
<url type="homepage">https://github.com/Scribery/cockpit-session-recording</url>
|
||||
<url type="bugtracker">https://github.com/Scribery/cockpit-session-recording/issues</url>
|
||||
<update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
|
||||
</component>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<component type="addon">
|
||||
<id>org.cockpit-project.starter-kit</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<name>Starter Kit</name>
|
||||
<summary>
|
||||
Scaffolding for a cockpit module.
|
||||
</summary>
|
||||
<description>
|
||||
<p>
|
||||
Scaffolding for a cockpit module.
|
||||
</p>
|
||||
</description>
|
||||
<extends>cockpit.desktop</extends>
|
||||
<launchable type="cockpit-manifest">cockpit-starter-kit</launchable>
|
||||
</component>
|
||||
89
package.json
89
package.json
|
|
@ -1,50 +1,67 @@
|
|||
{
|
||||
"name": "starter-kit",
|
||||
"version": "0.1.0",
|
||||
"description": "Scaffolding for a cockpit module",
|
||||
"name": "session-recording",
|
||||
"description": "Module for Cockpit which provides session recording configuration and playback",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:cockpit/starter-kit.git",
|
||||
"repository": "git@github.com:Scribery/cockpit-session-recording.git",
|
||||
"author": "",
|
||||
"license": "LGPL-2.1",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"eslint": "eslint --ext .jsx --ext .es6 src/",
|
||||
"eslint:fix": "eslint --fix --ext .jsx --ext .es6 src/"
|
||||
"watch": "ESBUILD_WATCH='true' ./build.js",
|
||||
"build": "./build.js",
|
||||
"eslint": "eslint --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.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"chrome-remote-interface": "^0.25.5",
|
||||
"compression-webpack-plugin": "^1.1.11",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^5.4.0",
|
||||
"eslint-config-standard": "^11.0.0",
|
||||
"eslint-config-standard-react": "^6.0.0",
|
||||
"eslint-loader": "^2.1.0",
|
||||
"eslint-plugin-flowtype": "^2.50.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-node": "^7.0.1",
|
||||
"eslint-plugin-promise": "^4.0.0",
|
||||
"eslint-plugin-react": "^6.9.0",
|
||||
"eslint-plugin-standard": "^3.1.0",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"argparse": "^2.0.1",
|
||||
"chrome-remote-interface": "^0.32.1",
|
||||
"esbuild": "0.18.6",
|
||||
"esbuild-plugin-copy": "^2.1.1",
|
||||
"esbuild-plugin-replace": "^1.3.0",
|
||||
"esbuild-sass-plugin": "2.10.0",
|
||||
"esbuild-wasm": "^0.18.6",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-standard": "^17.0.0-1",
|
||||
"eslint-config-standard-jsx": "^11.0.0-1",
|
||||
"eslint-config-standard-react": "^13.0.0",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
"gettext-parser": "2.0.0",
|
||||
"htmlparser": "^1.7.7",
|
||||
"jed": "^1.1.1",
|
||||
"po2json": "^0.4.5",
|
||||
"sass-loader": "^7.0.3",
|
||||
"qunit": "^2.9.3",
|
||||
"sass": "^1.61.0",
|
||||
"sizzle": "^2.3.3",
|
||||
"stdio": "^0.2.7",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-cli": "^3.1.0"
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-config-standard-scss": "^10.0.0",
|
||||
"stylelint-formatter-pretty": "^3.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"node-sass": "^4.9.0",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2"
|
||||
"@patternfly/patternfly": "5.0.4",
|
||||
"@patternfly/react-core": "5.0.1",
|
||||
"@patternfly/react-icons": "5.0.1",
|
||||
"@patternfly/react-styles": "5.0.1",
|
||||
"@patternfly/react-table": "5.0.1",
|
||||
"@patternfly/react-tokens": "5.0.1",
|
||||
"buffer": "^6.0.3",
|
||||
"comment-json": "^4.2.3",
|
||||
"date-fns": "^2.29.3",
|
||||
"ini": "^4.1.0",
|
||||
"jquery": "^3.6.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"xterm": "5.1.0",
|
||||
"xterm-addon-canvas": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
packaging/cockpit-session-recording.spec.in
Normal file
82
packaging/cockpit-session-recording.spec.in
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
Name: cockpit-session-recording
|
||||
Version: %{VERSION}
|
||||
Release: 1%{?dist}
|
||||
Summary: Cockpit Session Recording
|
||||
License: LGPL-2.1-or-later
|
||||
URL: https://github.com/Scribery/%{name}
|
||||
Source: https://github.com/Scribery/%{name}/releases/download/%{version}/%{name}-%{version}.tar.xz
|
||||
|
||||
BuildArch: noarch
|
||||
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 -q -n %{name}
|
||||
|
||||
%build
|
||||
LINT=0 NODE_ENV=production make
|
||||
|
||||
%install
|
||||
%make_install PREFIX=/usr
|
||||
appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/*
|
||||
|
||||
%files
|
||||
%{_datadir}/cockpit/*
|
||||
%{_datadir}/metainfo/*
|
||||
|
||||
%changelog
|
||||
* Wed Jan 13 2021 Justin Stephenson <jstephen@redhat.com> - 7-1
|
||||
- Release v7
|
||||
- Remove bots sudo rm from Makefile
|
||||
- Use journalctl --utc for Logs view to handle DST
|
||||
- Add Applications Menu test
|
||||
- Install cockpit-packagekit in local VM
|
||||
- Set timezone for Logs Correlation test
|
||||
|
||||
* Mon Oct 12 2020 Justin Stephenson <jstephen@redhat.com> - 6-1
|
||||
- Release v6
|
||||
- Bump testlib to 229
|
||||
- Add binary recording test
|
||||
|
||||
* Wed May 20 2020 Justin Stephenson <jstephen@redhat.com> - 4-1
|
||||
- Release v4
|
||||
- Update parent id in metainfo file
|
||||
- Update package manifest
|
||||
- Fix rpmmacro to resolve correc t path on CentOS7
|
||||
- Handle byte-array encoded journal data
|
||||
- Don't clobber cockpit bots directory
|
||||
- Move code out of deprecated React lifecycle functions
|
||||
|
||||
* Mon Nov 25 2019 Justin Stephenson <jstephen@redhat.com> - 3-1
|
||||
- Release v3
|
||||
- Reset Logs View on Player Rewind
|
||||
- Configuration page UI CSS Improvements
|
||||
|
||||
* Wed Sep 11 2019 Justin Stephenson <jstephen@redhat.com> - 2-1
|
||||
- Release 2
|
||||
- Optimize performance when playing back flooded output recordings.
|
||||
- Make Logs View optional rendered with a toggle button.
|
||||
- Make Logs component a child of Recording component.
|
||||
- Fix Recording page column sorting in Google Chrome.
|
||||
- CSS updates for Patternfly 4 compatibility.
|
||||
- Replace term.js with maintained xterm.js library.
|
||||
- Fix hostname and username filtering.
|
||||
|
||||
* Thu Apr 4 2019 Kirill Glebov <kgliebov@redhat.com> - 1-1
|
||||
- Release 1
|
||||
- First release. Includes logs correlation, player controls, journal remote support.
|
||||
70
packit.yaml
Normal file
70
packit.yaml
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Enable RPM builds and running integration tests in PRs through https://packit.dev/
|
||||
# To use this, enable Packit-as-a-service in GitHub: https://packit.dev/docs/packit-as-a-service/
|
||||
# See https://packit.dev/docs/configuration/ for the format of this file
|
||||
#
|
||||
upstream_project_url: https://github.com/Scribery/cockpit-session-recording
|
||||
# enable notification of failed downstream jobs as issues
|
||||
issue_repository: https://github.com/Scribery/cockpit-session-recording
|
||||
|
||||
specfile_path: cockpit-session-recording.spec
|
||||
upstream_package_name: cockpit-session-recording
|
||||
downstream_package_name: cockpit-session-recording
|
||||
# use the nicely formatted release description from our upstream release, instead of git shortlog
|
||||
copy_upstream_release_description: true
|
||||
|
||||
srpm_build_deps:
|
||||
- make
|
||||
- nodejs-npm
|
||||
|
||||
actions:
|
||||
post-upstream-clone:
|
||||
- make cockpit-session-recording.spec
|
||||
create-archive: make dist
|
||||
# starter-kit.git has no release tags; your project can drop this once you have a release
|
||||
get-current-version: make print-version
|
||||
|
||||
jobs:
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
targets:
|
||||
- fedora-all
|
||||
- fedora-latest-aarch64
|
||||
- centos-stream-8
|
||||
- centos-stream-9
|
||||
- centos-stream-9-aarch64
|
||||
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
targets:
|
||||
- fedora-all
|
||||
- fedora-latest-aarch64
|
||||
- centos-stream-8
|
||||
- centos-stream-9
|
||||
- centos-stream-9-aarch64
|
||||
|
||||
# Build releases in COPR: https://packit.dev/docs/configuration/#copr_build
|
||||
#- job: copr_build
|
||||
# trigger: release
|
||||
# owner: your_copr_login
|
||||
# project: your_copr_project
|
||||
# preserve_project: True
|
||||
# targets:
|
||||
# - fedora-all
|
||||
# - centos-stream-9-x86_64
|
||||
|
||||
# Build releases in Fedora: https://packit.dev/docs/configuration/#propose_downstream
|
||||
- job: propose_downstream
|
||||
trigger: release
|
||||
dist_git_branches:
|
||||
- fedora-all
|
||||
|
||||
- job: koji_build
|
||||
trigger: commit
|
||||
dist_git_branches:
|
||||
- fedora-all
|
||||
|
||||
- job: bodhi_update
|
||||
trigger: commit
|
||||
dist_git_branches:
|
||||
# rawhide updates are created automatically
|
||||
- fedora-branched
|
||||
10
plans/all.fmf
Normal file
10
plans/all.fmf
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
summary:
|
||||
Run all tests
|
||||
discover:
|
||||
how: fmf
|
||||
execute:
|
||||
how: tmt
|
||||
|
||||
# Let's handle them upstream only, don't break Fedora/RHEL reverse dependency gating
|
||||
environment:
|
||||
TEST_AUDIT_NO_SELINUX: 1
|
||||
15
po/de.po
15
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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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"
|
||||
|
|
|
|||
264
po/html2po
264
po/html2po
|
|
@ -1,264 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
* Extracts translatable strings from HTML files in the following forms:
|
||||
*
|
||||
* <tag translate>String</tag>
|
||||
* <tag translate context="value">String</tag>
|
||||
* <tag translate="...">String</tag>
|
||||
* <tag translate-attr attr="String"></tag>
|
||||
*
|
||||
* Supports the following Glade compatible forms:
|
||||
*
|
||||
* <tag translatable="yes">String</tag>
|
||||
* <tag translatable="yes" context="value">String</tag>
|
||||
*
|
||||
* Supports the following angular-gettext compatible forms:
|
||||
*
|
||||
* <translate>String</translate>
|
||||
* <tag translate-plural="Plural">Singular</tag>
|
||||
*
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
161
po/manifest2po
161
po/manifest2po
|
|
@ -1,161 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
* Extracts translatable strings from manifest.json files.
|
||||
*
|
||||
*/
|
||||
|
||||
function fatal(message, code) {
|
||||
console.log((filename || "manifest2po") + ": " + message);
|
||||
process.exit(code || 1);
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.log("usage: manifest2po [-o output] input...");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
var fs, path, stdio;
|
||||
|
||||
try {
|
||||
fs = require('fs');
|
||||
path = require('path');
|
||||
stdio = require('stdio');
|
||||
} catch (ex) {
|
||||
fatal(ex.message, 127); /* missing looks for this */
|
||||
}
|
||||
|
||||
var opts = stdio.getopt({
|
||||
directory: { key: "d", args: 1, description: "Base directory for input files" },
|
||||
output: { key: "o", args: 1, description: "Output file" },
|
||||
from: { key: "f", args: 1, description: "File containing list of input files" },
|
||||
});
|
||||
|
||||
if (!opts.from && opts.args.length < 1) {
|
||||
usage();
|
||||
}
|
||||
|
||||
var input = opts.args;
|
||||
var entries = { };
|
||||
|
||||
/* Filename being parsed */
|
||||
var filename = null;
|
||||
|
||||
prepare();
|
||||
|
||||
/* Decide what input files to process */
|
||||
function prepare() {
|
||||
if (opts.from) {
|
||||
fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) {
|
||||
if (err)
|
||||
fatal(err.message);
|
||||
input = data.split("\n").filter(function(value) {
|
||||
return !!value;
|
||||
}).concat(input);
|
||||
step();
|
||||
});
|
||||
} else {
|
||||
step();
|
||||
}
|
||||
}
|
||||
|
||||
/* Now process each file in turn */
|
||||
function step() {
|
||||
filename = input.shift();
|
||||
if (filename === undefined) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.basename(filename) != "manifest.json")
|
||||
return step();
|
||||
|
||||
fs.readFile(filename, { encoding: "utf-8"}, function(err, data) {
|
||||
if (err)
|
||||
fatal(err.message);
|
||||
|
||||
process_manifest(JSON.parse(data));
|
||||
|
||||
return step();
|
||||
});
|
||||
}
|
||||
|
||||
function process_manifest(manifest) {
|
||||
if (manifest.menu)
|
||||
process_menu(manifest.menu);
|
||||
if (manifest.tools)
|
||||
process_menu(manifest.tools);
|
||||
}
|
||||
|
||||
function process_menu(menu) {
|
||||
for (var m in menu) {
|
||||
if (menu[m].label) {
|
||||
push({
|
||||
msgid: menu[m].label,
|
||||
locations: [ filename ]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Push an entry onto the list */
|
||||
function push(entry) {
|
||||
var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
|
||||
var prev = entries[key];
|
||||
if (prev) {
|
||||
prev.locations = prev.locations.concat(entry.locations);
|
||||
} else {
|
||||
entries[key] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
/* Escape a string for inclusion in po file */
|
||||
function escape(string) {
|
||||
var bs = string.split('\\').join('\\\\').split('"').join('\\"');
|
||||
return bs.split("\n").map(function(line) {
|
||||
return '"' + line + '"';
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
/* Finish by writing out the strings */
|
||||
function finish() {
|
||||
var result = [
|
||||
'msgid ""',
|
||||
'msgstr ""',
|
||||
'"Project-Id-Version: PACKAGE_VERSION\\n"',
|
||||
'"MIME-Version: 1.0\\n"',
|
||||
'"Content-Type: text/plain; charset=UTF-8\\n"',
|
||||
'"Content-Transfer-Encoding: 8bit\\n"',
|
||||
'"X-Generator: Cockpit manifest2po\\n"',
|
||||
'',
|
||||
];
|
||||
|
||||
var msgid, entry;
|
||||
for (msgid in entries) {
|
||||
entry = entries[msgid];
|
||||
result.push('#: ' + entry.locations.join(" "));
|
||||
if (entry.msgctxt)
|
||||
result.push('msgctxt ' + escape(entry.msgctxt));
|
||||
result.push('msgid ' + escape(entry.msgid));
|
||||
if (entry.msgid_plural) {
|
||||
result.push('msgid_plural ' + escape(entry.msgid_plural));
|
||||
result.push('msgstr[0] ""');
|
||||
result.push('msgstr[1] ""');
|
||||
} else {
|
||||
result.push('msgstr ""');
|
||||
}
|
||||
result.push('');
|
||||
}
|
||||
|
||||
var data = result.join('\n');
|
||||
if (!opts.output) {
|
||||
process.stdout.write(data);
|
||||
process.exit(0);
|
||||
} else {
|
||||
fs.writeFile(opts.output, data, function(err) {
|
||||
if (err)
|
||||
fatal(err.message);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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"}}));
|
||||
127
po/po2json
127
po/po2json
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
16
src/app.jsx
16
src/app.jsx
|
|
@ -19,29 +19,23 @@
|
|||
|
||||
import cockpit from 'cockpit';
|
||||
import React from 'react';
|
||||
import './app.scss';
|
||||
import View from "./recordings.jsx";
|
||||
|
||||
const _ = cockpit.gettext;
|
||||
|
||||
export class Application extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { 'hostname': _("Unknown") };
|
||||
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() {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<h2>Starter Kit</h2>
|
||||
<p>
|
||||
{ cockpit.format(_("Running on $0"), this.state.hostname) }
|
||||
</p>
|
||||
</div>
|
||||
<View />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/app.scss
15
src/app.scss
|
|
@ -1,3 +1,18 @@
|
|||
@use "page.scss";
|
||||
|
||||
p {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Ensure UI fills the entire page (and does not run over)
|
||||
.ct-page-fill {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
row-gap: var(--pf-global--spacer--sm);
|
||||
|
||||
> .pf-c-card {
|
||||
min-width: 30rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
662
src/config.jsx
Normal file
662
src/config.jsx
Normal file
|
|
@ -0,0 +1,662 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2017 Red Hat, Inc.
|
||||
*
|
||||
* Cockpit is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Cockpit is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Breadcrumb, BreadcrumbItem,
|
||||
Button,
|
||||
Flex,
|
||||
Form,
|
||||
FormGroup,
|
||||
FormSelect,
|
||||
FormSelectOption,
|
||||
TextInput,
|
||||
ActionGroup,
|
||||
Spinner,
|
||||
Card,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
Checkbox,
|
||||
Bullseye,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody,
|
||||
EmptyStateVariant,
|
||||
Page, PageSection, EmptyStateHeader,
|
||||
} from "@patternfly/react-core";
|
||||
import { ExclamationCircleIcon } from "@patternfly/react-icons";
|
||||
import { global_danger_color_200 } from "@patternfly/react-tokens";
|
||||
import cockpit from 'cockpit';
|
||||
|
||||
const json = require('comment-json');
|
||||
const ini = require('ini');
|
||||
const _ = cockpit.gettext;
|
||||
|
||||
class GeneralConfig extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.setConfig = this.setConfig.bind(this);
|
||||
this.fileReadFailed = this.fileReadFailed.bind(this);
|
||||
this.readConfig = this.readConfig.bind(this);
|
||||
this.file = null;
|
||||
this.config = null;
|
||||
this.state = {
|
||||
config_loaded: false,
|
||||
file_error: false,
|
||||
submitting: false,
|
||||
shell: "",
|
||||
notice: "",
|
||||
latency: "",
|
||||
payload: "",
|
||||
log_input: false,
|
||||
log_output: true,
|
||||
log_window: true,
|
||||
limit_rate: "",
|
||||
limit_burst: "",
|
||||
limit_action: "",
|
||||
file_path: "",
|
||||
syslog_facility: "",
|
||||
syslog_priority: "",
|
||||
journal_augment: "",
|
||||
journal_priority: "",
|
||||
writer: "",
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
this.setState({ submitting: true });
|
||||
const config = {
|
||||
shell: this.state.shell,
|
||||
notice: this.state.notice,
|
||||
latency: parseInt(this.state.latency),
|
||||
payload: parseInt(this.state.payload),
|
||||
log: {
|
||||
input: this.state.log_input,
|
||||
output: this.state.log_output,
|
||||
window: this.state.log_window,
|
||||
},
|
||||
limit: {
|
||||
rate: parseInt(this.state.limit_rate),
|
||||
burst: parseInt(this.state.limit_burst),
|
||||
action: this.state.limit_action,
|
||||
},
|
||||
file: {
|
||||
path: this.state.file_path,
|
||||
},
|
||||
syslog: {
|
||||
facility: this.state.syslog_facility,
|
||||
priority: this.state.syslog_priority,
|
||||
},
|
||||
journal: {
|
||||
priority: this.state.journal_priority,
|
||||
augment: this.state.journal_augment
|
||||
},
|
||||
writer: this.state.writer
|
||||
};
|
||||
this.file.replace(config).done(() => {
|
||||
this.setState({ submitting: false });
|
||||
})
|
||||
.fail((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
setConfig(data) {
|
||||
delete data.configuration;
|
||||
delete data.args;
|
||||
const flattenObject = function(ob) {
|
||||
const toReturn = {};
|
||||
|
||||
for (const i in ob) {
|
||||
if (!Object.prototype.hasOwnProperty.call(ob, i)) continue;
|
||||
|
||||
if ((typeof ob[i]) === 'object') {
|
||||
const flatObject = flattenObject(ob[i]);
|
||||
for (const x in flatObject) {
|
||||
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) continue;
|
||||
|
||||
toReturn[i + '_' + x] = flatObject[x];
|
||||
}
|
||||
} else {
|
||||
toReturn[i] = ob[i];
|
||||
}
|
||||
}
|
||||
return toReturn;
|
||||
};
|
||||
const state = flattenObject(data);
|
||||
state.config_loaded = true;
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
const proc = cockpit.spawn(["tlog-rec-session", "--configuration"]);
|
||||
|
||||
proc.stream((data) => {
|
||||
this.setConfig(json.parse(data, null, true));
|
||||
proc.close();
|
||||
});
|
||||
|
||||
proc.fail((fail) => {
|
||||
console.log(fail);
|
||||
this.readConfig();
|
||||
});
|
||||
}
|
||||
|
||||
readConfig() {
|
||||
const parseFunc = function(data) {
|
||||
return json.parse(data, null, true);
|
||||
};
|
||||
|
||||
const stringifyFunc = function(data) {
|
||||
return json.stringify(data, null, true);
|
||||
};
|
||||
// needed for cockpit.file usage
|
||||
const syntax_object = {
|
||||
parse: parseFunc,
|
||||
stringify: stringifyFunc,
|
||||
};
|
||||
|
||||
this.file = cockpit.file("/etc/tlog/tlog-rec-session.conf", {
|
||||
syntax: syntax_object,
|
||||
superuser: true,
|
||||
});
|
||||
}
|
||||
|
||||
fileReadFailed(reason) {
|
||||
console.log(reason);
|
||||
this.setState({ file_error: reason });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getConfig();
|
||||
this.readConfig();
|
||||
}
|
||||
|
||||
handleInputChange(name, value) {
|
||||
const state = {};
|
||||
state[name] = value;
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
render() {
|
||||
const form =
|
||||
(this.state.config_loaded === false && this.state.file_error === false)
|
||||
? <Spinner />
|
||||
: (this.state.config_loaded === true && this.state.file_error === false)
|
||||
? (
|
||||
<Form isHorizontal>
|
||||
<FormGroup label={_("Shell")}>
|
||||
<TextInput
|
||||
id="shell"
|
||||
value={this.state.shell}
|
||||
onChange={(_event, value) => this.handleInputChange("shell", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Notice")}>
|
||||
<TextInput
|
||||
id="notice"
|
||||
value={this.state.notice}
|
||||
onChange={(_event, value) => this.handleInputChange("notice", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Latency")}>
|
||||
<TextInput
|
||||
id="latency"
|
||||
type="number"
|
||||
step="1"
|
||||
value={this.state.latency}
|
||||
onChange={(_event, value) => this.handleInputChange("latency", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Payload Size, bytes")}>
|
||||
<TextInput
|
||||
id="payload"
|
||||
type="number"
|
||||
step="1"
|
||||
value={this.state.payload}
|
||||
onChange={(_event, value) => this.handleInputChange("payload", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Logging")}>
|
||||
<Checkbox
|
||||
id="log_input"
|
||||
isChecked={this.state.log_input}
|
||||
onChange={(_event, log_input) => this.setState({ log_input })}
|
||||
label={_("User's Input")}
|
||||
/>
|
||||
<Checkbox
|
||||
id="log_output"
|
||||
isChecked={this.state.log_output}
|
||||
onChange={(_event, log_output) => this.setState({ log_output })}
|
||||
label={_("User's Output")}
|
||||
/>
|
||||
<Checkbox
|
||||
id="log_window"
|
||||
isChecked={this.state.log_window}
|
||||
onChange={(_event, log_window) => this.setState({ log_window })}
|
||||
label={_("Window Resize")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Limit Rate, bytes/sec")}>
|
||||
<TextInput
|
||||
id="limit_rate"
|
||||
type="number"
|
||||
step="1"
|
||||
value={this.state.limit_rate}
|
||||
onChange={(_event, value) => this.handleInputChange("limit_rate", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Burst, bytes")}>
|
||||
<TextInput
|
||||
id="limit_burst"
|
||||
type="number"
|
||||
step="1"
|
||||
value={this.state.limit_burst}
|
||||
onChange={(_event, value) => this.handleInputChange("limit_burst", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Logging Limit Action")}>
|
||||
<FormSelect
|
||||
id="limit_action"
|
||||
value={this.state.limit_action}
|
||||
onChange={(_event, value) => this.handleInputChange("limit_action", value)}
|
||||
>
|
||||
{[
|
||||
{ value: "", label: "" },
|
||||
{ value: "pass", label: _("Pass") },
|
||||
{ value: "delay", label: _("Delay") },
|
||||
{ value: "drop", label: _("Drop") }
|
||||
].map((option, index) =>
|
||||
<FormSelectOption
|
||||
key={index}
|
||||
value={option.value}
|
||||
label={option.label}
|
||||
/>
|
||||
)}
|
||||
</FormSelect>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("File Path")}>
|
||||
<TextInput
|
||||
id="file_path"
|
||||
value={this.state.file_path}
|
||||
onChange={(_event, value) => this.handleInputChange("file_path", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Syslog Facility")}>
|
||||
<TextInput
|
||||
id="syslog_facility"
|
||||
value={this.state.syslog_facility}
|
||||
onChange={(_event, value) => this.handleInputChange("syslog_facility", value)}
|
||||
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Syslog Priority")}>
|
||||
<FormSelect
|
||||
id="syslog_priority"
|
||||
value={this.state.syslog_priority}
|
||||
onChange={(_event, value) => this.handleInputChange("syslog_priority", value)}
|
||||
>
|
||||
{[
|
||||
{ value: "", label: "" },
|
||||
{ value: "info", label: _("Info") },
|
||||
].map((option, index) =>
|
||||
<FormSelectOption
|
||||
key={index}
|
||||
value={option.value}
|
||||
label={option.label}
|
||||
/>
|
||||
)}
|
||||
</FormSelect>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Journal Priority")}>
|
||||
<FormSelect
|
||||
id="journal_priority"
|
||||
value={this.state.journal_priority}
|
||||
onChange={(_event, value) => this.handleInputChange("journal_priority", value)}
|
||||
|
||||
>
|
||||
{[
|
||||
{ value: "", label: "" },
|
||||
{ value: "info", label: _("Info") },
|
||||
].map((option, index) =>
|
||||
<FormSelectOption
|
||||
key={index}
|
||||
value={option.value}
|
||||
label={option.label}
|
||||
/>
|
||||
)}
|
||||
</FormSelect>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Checkbox
|
||||
id="journal_augment"
|
||||
isChecked={this.state.journal_augment}
|
||||
onChange={(_event, journal_augment) => this.setState({ journal_augment })}
|
||||
label={_("Augment")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Writer")}>
|
||||
<FormSelect
|
||||
id="writer"
|
||||
value={this.state.writer}
|
||||
onChange={(_event, value) => this.handleInputChange("writer", value)}
|
||||
>
|
||||
{[
|
||||
{ value: "", label: "" },
|
||||
{ value: "journal", label: _("Journal") },
|
||||
{ value: "syslog", label: _("Syslog") },
|
||||
{ value: "file", label: _("File") },
|
||||
].map((option, index) =>
|
||||
<FormSelectOption
|
||||
key={index}
|
||||
value={option.value}
|
||||
label={option.label}
|
||||
/>
|
||||
)}
|
||||
</FormSelect>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
id="btn-save-tlog-conf"
|
||||
variant="primary"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{_("Save")}
|
||||
</Button>
|
||||
{this.state.submitting === true && <Spinner size="lg" />}
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
)
|
||||
: (
|
||||
<Bullseye>
|
||||
<EmptyState variant={EmptyStateVariant.sm}>
|
||||
<EmptyStateHeader
|
||||
titleText={<>{_("There is no configuration file of tlog present in your system.")}</>}
|
||||
icon={
|
||||
<EmptyStateIcon
|
||||
icon={ExclamationCircleIcon}
|
||||
color={global_danger_color_200.value}
|
||||
/>
|
||||
} headingLevel="h4"
|
||||
/>
|
||||
<EmptyStateHeader titleText={<>{_("Please, check the /etc/tlog/tlog-rec-session.conf or if tlog is installed.")}</>} headingLevel="h4" />
|
||||
<EmptyStateBody>
|
||||
{this.state.file_error}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
</Bullseye>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle>General Config</CardTitle>
|
||||
<CardBody>{form}</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SssdConfig extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.confSave = this.confSave.bind(this);
|
||||
this.restartSSSD = this.restartSSSD.bind(this);
|
||||
this.file = null;
|
||||
this.state = {
|
||||
scope: "",
|
||||
users: "",
|
||||
exclude_users: "",
|
||||
exclude_groups: "",
|
||||
groups: "",
|
||||
submitting: false,
|
||||
};
|
||||
}
|
||||
|
||||
customIniUnparser(obj) {
|
||||
return ini.stringify(obj, { platform: 'linux' }).replace('domainnssfiles', 'domain/nssfiles');
|
||||
}
|
||||
|
||||
restartSSSD() {
|
||||
const sssd_cmd = ["systemctl", "restart", "sssd"];
|
||||
cockpit.spawn(sssd_cmd, { superuser: "require" });
|
||||
this.setState({ submitting: false });
|
||||
}
|
||||
|
||||
confSave(obj) {
|
||||
const chmod_cmd = ["chmod", "600", "/etc/sssd/conf.d/sssd-session-recording.conf"];
|
||||
/* Update nsswitch, this will fail on RHEL8/F34 and lower as 'with-files-domain' feature is not added there */
|
||||
const authselect_cmd = ["authselect", "select", "sssd", "with-files-domain", "--force"];
|
||||
this.setState({ submitting: true });
|
||||
this.file.replace(obj)
|
||||
.then(tag => {
|
||||
cockpit.spawn(chmod_cmd, { superuser: "require" })
|
||||
.then(() => {
|
||||
cockpit.spawn(authselect_cmd, { superuser: "require" })
|
||||
.then(this.restartSSSD)
|
||||
.catch(this.restartSSSD);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const syntax_object = {
|
||||
parse: ini.parse,
|
||||
stringify: this.customIniUnparser,
|
||||
};
|
||||
|
||||
this.file = cockpit.file("/etc/sssd/conf.d/sssd-session-recording.conf", {
|
||||
syntax: syntax_object,
|
||||
superuser: true,
|
||||
});
|
||||
|
||||
const conf_syntax_object = {
|
||||
parse: ini.parse,
|
||||
};
|
||||
|
||||
this.sssdconf = cockpit.file("/etc/sssd/sssd.conf", {
|
||||
syntax: conf_syntax_object,
|
||||
superuser: true,
|
||||
});
|
||||
|
||||
const promise = this.file.read();
|
||||
const sssdconfpromise = this.sssdconf.read();
|
||||
|
||||
promise.fail(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
/* It is not an error when the file does not exist, then() callback will
|
||||
* be called with a null value for content and tag is "-" */
|
||||
sssdconfpromise
|
||||
.then((content, tag) => {
|
||||
if (content !== null) {
|
||||
this.existingServices = content.sssd.services;
|
||||
this.existingDomains = content.sssd.domains;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("Error: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
const obj = {};
|
||||
/* SSSD section */
|
||||
obj.sssd = {};
|
||||
/* Avoid overwriting services and domain sections of existing sssd.conf
|
||||
* Copy the services section used in sssd.conf, and append 'proxy' to
|
||||
* existing domain section */
|
||||
if (this.existingServices) {
|
||||
obj.sssd.services = this.existingServices;
|
||||
} else {
|
||||
obj.sssd.services = "nss, pam";
|
||||
}
|
||||
|
||||
if (this.existingDomains) {
|
||||
obj.sssd.domains = this.existingDomains + ", nssfiles";
|
||||
} else {
|
||||
obj.sssd.domains = "nssfiles";
|
||||
}
|
||||
/* Proxy provider */
|
||||
obj.domainnssfiles = {}; /* Unparser converts this into domain/nssfiles */
|
||||
obj.domainnssfiles.id_provider = "proxy";
|
||||
obj.domainnssfiles.proxy_lib_name = "files";
|
||||
obj.domainnssfiles.proxy_pam_target = "sssd-shadowutils";
|
||||
/* Session recording */
|
||||
obj.session_recording = {};
|
||||
obj.session_recording.scope = this.state.scope;
|
||||
switch (this.state.scope) {
|
||||
case "all":
|
||||
obj.session_recording.exclude_users = this.state.exclude_users;
|
||||
obj.session_recording.exclude_groups = this.state.exclude_groups;
|
||||
break;
|
||||
case "none":
|
||||
break;
|
||||
case "some":
|
||||
obj.session_recording.users = this.state.users;
|
||||
obj.session_recording.groups = this.state.groups;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.confSave(obj);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
handleInputChange(name, value) {
|
||||
const state = {};
|
||||
state[name] = value;
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
render() {
|
||||
const form = (
|
||||
<Form isHorizontal>
|
||||
<FormGroup label="Scope">
|
||||
<FormSelect
|
||||
id="scope"
|
||||
value={this.state.scope}
|
||||
onChange={(_event, value) => this.handleInputChange("scope", value)}
|
||||
>
|
||||
{[
|
||||
{ value: "none", label: _("None") },
|
||||
{ value: "some", label: _("Some") },
|
||||
{ value: "all", label: _("All") }
|
||||
].map((option, index) =>
|
||||
<FormSelectOption
|
||||
key={index}
|
||||
value={option.value}
|
||||
label={option.label}
|
||||
/>
|
||||
)}
|
||||
</FormSelect>
|
||||
</FormGroup>
|
||||
{this.state.scope === "some" &&
|
||||
<>
|
||||
<FormGroup label={_("Users")}>
|
||||
<TextInput
|
||||
id="users"
|
||||
value={this.state.users}
|
||||
onChange={(_event, value) => this.handleInputChange("users", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Groups")}>
|
||||
<TextInput
|
||||
id="groups"
|
||||
value={this.state.groups}
|
||||
onChange={(_event, value) => this.handleInputChange("groups", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>}
|
||||
{this.state.scope === "all" &&
|
||||
<>
|
||||
<FormGroup label={_("Exclude Users")}>
|
||||
<TextInput
|
||||
id="exclude_users"
|
||||
value={this.state.exclude_users}
|
||||
onChange={(_event, value) => this.handleInputChange("exclude_users", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={_("Exclude Groups")}>
|
||||
<TextInput
|
||||
id="exclude_groups"
|
||||
value={this.state.exclude_groups}
|
||||
onChange={(_event, value) => this.handleInputChange("exclude_groups", value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
id="btn-save-sssd-conf"
|
||||
variant="primary"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{_("Save")}
|
||||
</Button>
|
||||
{this.state.submitting === true && <Spinner size="lg" />}
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle>SSSD Config</CardTitle>
|
||||
<CardBody>{form}</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function Config () {
|
||||
const goBack = () => {
|
||||
cockpit.location.go("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
groupProps={{ sticky: 'top' }}
|
||||
isBreadcrumbGrouped
|
||||
breadcrumb={
|
||||
<Breadcrumb className='machines-listing-breadcrumb'>
|
||||
<BreadcrumbItem to='#' onClick={goBack}>
|
||||
{_("Session Recording")}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem isActive>
|
||||
{_("Settings")}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
}
|
||||
>
|
||||
<PageSection>
|
||||
<Flex className="config-container">
|
||||
<GeneralConfig />
|
||||
<SssdConfig />
|
||||
</Flex>
|
||||
</PageSection>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
@ -17,17 +17,15 @@ along with this package; If not, see <http://www.gnu.org/licenses/>.
|
|||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title translatable="yes">Cockpit Starter Kit</title>
|
||||
<title translate>Cockpit Session Recording</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="../base1/patternfly.css">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
|
||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
||||
<script type="text/javascript" src="../*/po.js"></script>
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
<script type="text/javascript" src="po.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@
|
|||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Application } from './app.jsx';
|
||||
import "cockpit-dark-theme";
|
||||
import "patternfly/patternfly-5-cockpit.scss";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
ReactDOM.render(React.createElement(Application, {}), document.getElementById('app'));
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Application } from './app.jsx';
|
||||
import './app.scss';
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
createRoot(document.getElementById("app")).render(<Application />);
|
||||
});
|
||||
|
|
@ -1,12 +1,26 @@
|
|||
{
|
||||
"version": "0.1",
|
||||
"version": "163.x",
|
||||
"name": "session-recording",
|
||||
|
||||
"requires": {
|
||||
"cockpit": "137"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"menu": {
|
||||
"index": {
|
||||
"label": "Starter Kit"
|
||||
"label": "Session Recording",
|
||||
"order": 110,
|
||||
"docs": [
|
||||
{
|
||||
"label": "Recording sessions",
|
||||
"url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/recording_sessions/index"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
{
|
||||
"matches": ["tlog", "sssd"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
src/player.css
Normal file
84
src/player.css
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
@import "xterm/css/xterm.css";
|
||||
|
||||
.player-wrap {
|
||||
min-width: 672px;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.player-wrap .panel-body, .player-wrap .console-ct > .terminal {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dragnpan {
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#logs-view {
|
||||
height: 300px;
|
||||
overflow-y: scroll;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#recording-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logs-view-log-time {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#input-textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: monospace;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
#input-player-wrap {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
margin-left: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
float: left;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
.search-component {
|
||||
float: left;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
min-height: 25px;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.session_time {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.pf-c-progress__indicator::after {
|
||||
content: "";
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--pf-c-progress__indicator--BackgroundColor);
|
||||
top: -2px;
|
||||
left: 10px;
|
||||
float: right;
|
||||
}
|
||||
1509
src/player.jsx
Normal file
1509
src/player.jsx
Normal file
File diff suppressed because it is too large
Load diff
949
src/recordings.jsx
Normal file
949
src/recordings.jsx
Normal file
|
|
@ -0,0 +1,949 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2017 Red Hat, Inc.
|
||||
*
|
||||
* Cockpit is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Cockpit is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from "react";
|
||||
import {
|
||||
Breadcrumb, BreadcrumbItem,
|
||||
Bullseye,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
DataList,
|
||||
DataListCell,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
EmptyStateVariant,
|
||||
ExpandableSection,
|
||||
Page, PageSection, PageSectionVariants,
|
||||
Spinner,
|
||||
TextInput,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
ToolbarGroup, EmptyStateHeader,
|
||||
} from "@patternfly/react-core";
|
||||
import { sortable, SortByDirection } from '@patternfly/react-table';
|
||||
import { TableHeader, TableBody, Table as TableDeprecated } from '@patternfly/react-table/deprecated';
|
||||
import {
|
||||
CogIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
PlusIcon,
|
||||
SearchIcon
|
||||
} from "@patternfly/react-icons";
|
||||
import cockpit from 'cockpit';
|
||||
import { global_danger_color_200 } from "@patternfly/react-tokens";
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import { journal } from 'journal';
|
||||
|
||||
const $ = require("jquery");
|
||||
const _ = cockpit.gettext;
|
||||
const Player = require("./player.jsx");
|
||||
const Config = require("./config.jsx");
|
||||
|
||||
/*
|
||||
* Convert a number to integer number string and pad with zeroes to
|
||||
* specified width.
|
||||
*/
|
||||
const padInt = function (n, w) {
|
||||
const i = Math.floor(n);
|
||||
const a = Math.abs(i);
|
||||
let s = a.toString();
|
||||
for (w -= s.length; w > 0; w--) {
|
||||
s = '0' + s;
|
||||
}
|
||||
return ((i < 0) ? '-' : '') + s;
|
||||
};
|
||||
|
||||
/*
|
||||
* Format date and time for a number of milliseconds since Epoch.
|
||||
* YYYY-MM-DD HH:mm:ss
|
||||
*/
|
||||
const formatDateTime = function (ms) {
|
||||
/* Convert local timezone offset */
|
||||
const t = new Date(ms);
|
||||
const z = t.getTimezoneOffset() * 60 * 1000;
|
||||
let tLocal = t - z;
|
||||
tLocal = new Date(tLocal);
|
||||
let iso = tLocal.toISOString();
|
||||
|
||||
/* cleanup ISO format */
|
||||
iso = iso.slice(0, 19);
|
||||
iso = iso.replace('T', ' ');
|
||||
return iso;
|
||||
};
|
||||
|
||||
const formatUTC = function(date) {
|
||||
let iso = null;
|
||||
try {
|
||||
iso = new Date(date).toISOString();
|
||||
iso = iso.slice(0, 19);
|
||||
iso = iso.replace('T', ' ') + " UTC";
|
||||
} catch (error) {
|
||||
iso = "";
|
||||
}
|
||||
|
||||
return iso;
|
||||
};
|
||||
|
||||
/*
|
||||
* Format a time interval from a number of milliseconds.
|
||||
*/
|
||||
const formatDuration = function (ms) {
|
||||
let v = Math.floor(ms / 1000);
|
||||
const s = Math.floor(v % 60);
|
||||
v = Math.floor(v / 60);
|
||||
const m = Math.floor(v % 60);
|
||||
v = Math.floor(v / 60);
|
||||
const h = Math.floor(v % 24);
|
||||
const d = Math.floor(v / 24);
|
||||
let str = '';
|
||||
|
||||
if (d > 0) {
|
||||
str += d + ' ' + _("days") + ' ';
|
||||
}
|
||||
|
||||
if (h > 0 || str.length > 0) {
|
||||
str += padInt(h, 2) + ':';
|
||||
}
|
||||
|
||||
str += padInt(m, 2) + ':' + padInt(s, 2);
|
||||
|
||||
return (ms < 0 ? '-' : '') + str;
|
||||
};
|
||||
|
||||
function LogElement(props) {
|
||||
const entry = props.entry;
|
||||
const start = props.start;
|
||||
const cursor = entry.__CURSOR;
|
||||
const entry_timestamp = parseInt(entry.__REALTIME_TIMESTAMP / 1000);
|
||||
|
||||
const timeClick = (_e) => {
|
||||
const ts = entry_timestamp - start;
|
||||
if (ts > 0) {
|
||||
props.onJumpToTs(ts);
|
||||
} else {
|
||||
props.onJumpToTs(0);
|
||||
}
|
||||
};
|
||||
const messageClick = () => {
|
||||
const url = '/system/logs#/' + cursor + '?parent_options={}';
|
||||
const win = window.open(url, '_blank');
|
||||
win.focus();
|
||||
};
|
||||
|
||||
const cells = (
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key="row">
|
||||
<ExclamationTriangleIcon />
|
||||
<Button variant="link" onClick={timeClick}>
|
||||
{formatDateTime(entry_timestamp)}
|
||||
</Button>
|
||||
<Card isSelectable onClick={messageClick}>
|
||||
<CardBody>{entry.MESSAGE}</CardBody>
|
||||
</Card>
|
||||
</DataListCell>
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<DataListItem>
|
||||
<DataListItemRow>{cells}</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function LogsView(props) {
|
||||
const { entries, start, end } = props;
|
||||
const rows = entries.map((entry) =>
|
||||
<LogElement
|
||||
key={entry.__CURSOR}
|
||||
entry={entry}
|
||||
start={start}
|
||||
end={end}
|
||||
onJumpToTs={props.onJumpToTs}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<DataList>{rows}</DataList>
|
||||
);
|
||||
}
|
||||
|
||||
class Logs extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.journalctlError = this.journalctlError.bind(this);
|
||||
this.journalctlIngest = this.journalctlIngest.bind(this);
|
||||
this.journalctlPrepend = this.journalctlPrepend.bind(this);
|
||||
this.getLogs = this.getLogs.bind(this);
|
||||
this.handleLoadLater = this.handleLoadLater.bind(this);
|
||||
this.loadForTs = this.loadForTs.bind(this);
|
||||
this.journalCtl = null;
|
||||
this.entries = [];
|
||||
this.start = null;
|
||||
this.end = null;
|
||||
this.hostname = null;
|
||||
this.state = {
|
||||
serverTimeOffset: null,
|
||||
cursor: null,
|
||||
after: null,
|
||||
entries: [],
|
||||
};
|
||||
}
|
||||
|
||||
journalctlError(error) {
|
||||
console.warn(cockpit.message(error));
|
||||
}
|
||||
|
||||
journalctlIngest(entryList) {
|
||||
if (entryList.length > 0) {
|
||||
this.entries.push(...entryList);
|
||||
const after = this.entries[this.entries.length - 1].__CURSOR;
|
||||
this.setState({ entries: this.entries, after });
|
||||
}
|
||||
}
|
||||
|
||||
journalctlPrepend(entryList) {
|
||||
entryList.push(...this.entries);
|
||||
this.setState({ entries: this.entries });
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
if (this.start != null && this.end != null) {
|
||||
if (this.journalCtl != null) {
|
||||
this.journalCtl.stop();
|
||||
this.journalCtl = null;
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
if (this.hostname) {
|
||||
matches.push("_HOSTNAME=" + this.hostname);
|
||||
}
|
||||
|
||||
let start = null;
|
||||
let end = null;
|
||||
|
||||
start = formatDateTime(this.start);
|
||||
end = formatDateTime(this.end);
|
||||
|
||||
const options = {
|
||||
since: start,
|
||||
until: end,
|
||||
follow: false,
|
||||
count: "all",
|
||||
merge: true,
|
||||
utc: true,
|
||||
};
|
||||
|
||||
if (this.state.after != null) {
|
||||
options.after = this.state.after;
|
||||
delete options.since;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
this.journalCtl = journal.journalctl(matches, options)
|
||||
.fail(this.journalctlError)
|
||||
.done(function(data) {
|
||||
self.journalctlIngest(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadLater() {
|
||||
this.start = this.end;
|
||||
this.end = this.end + 3600;
|
||||
this.getLogs();
|
||||
}
|
||||
|
||||
loadForTs(ts) {
|
||||
this.end = this.start + ts;
|
||||
this.getLogs();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.recording) {
|
||||
if (this.start === null && this.end === null) {
|
||||
this.end = this.props.recording.start + 3600;
|
||||
this.start = this.props.recording.start;
|
||||
}
|
||||
if (this.props.recording.hostname) {
|
||||
this.hostname = this.props.recording.hostname;
|
||||
}
|
||||
this.getLogs();
|
||||
}
|
||||
if (this.props.curTs) {
|
||||
const ts = this.props.curTs;
|
||||
this.loadForTs(ts);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.journalCtl) {
|
||||
this.journalCtl.stop();
|
||||
}
|
||||
this.setState({
|
||||
serverTimeOffset: null,
|
||||
cursor: null,
|
||||
after: null,
|
||||
entries: [],
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const r = this.props.recording;
|
||||
if (r == null) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<EmptyState variant={EmptyStateVariant.sm}>
|
||||
<Spinner />
|
||||
<EmptyStateHeader titleText={<>{_("Loading...")}</>} headingLevel="h2" />
|
||||
</EmptyState>
|
||||
</Bullseye>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<LogsView
|
||||
id="logs-view"
|
||||
entries={this.state.entries}
|
||||
start={this.props.recording.start}
|
||||
end={this.props.recording.end}
|
||||
onJumpToTs={this.props.onJumpToTs}
|
||||
/>
|
||||
<Bullseye>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<PlusIcon />}
|
||||
onClick={this.handleLoadLater}
|
||||
>
|
||||
{_("Load later entries")}
|
||||
</Button>
|
||||
</Bullseye>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A component representing a single recording view.
|
||||
* Properties:
|
||||
* - recording: either null for no recording data available yet, or a
|
||||
* recording object, as created by the View below.
|
||||
*/
|
||||
class Recording extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleGoBackToList = this.handleGoBackToList.bind(this);
|
||||
this.handleTsChange = this.handleTsChange.bind(this);
|
||||
this.handleLogTsChange = this.handleLogTsChange.bind(this);
|
||||
this.handleLogsClick = this.handleLogsClick.bind(this);
|
||||
this.handleLogsReset = this.handleLogsReset.bind(this);
|
||||
this.playerRef = React.createRef();
|
||||
this.state = {
|
||||
curTs: null,
|
||||
logsTs: null,
|
||||
logsEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleTsChange(ts) {
|
||||
this.setState({ curTs: ts });
|
||||
}
|
||||
|
||||
handleLogTsChange(ts) {
|
||||
this.setState({ logsTs: ts });
|
||||
}
|
||||
|
||||
handleLogsClick() {
|
||||
this.setState({ logsEnabled: !this.state.logsEnabled });
|
||||
}
|
||||
|
||||
handleLogsReset() {
|
||||
this.setState({ logsEnabled: false }, () => {
|
||||
this.setState({ logsEnabled: true });
|
||||
});
|
||||
}
|
||||
|
||||
handleGoBackToList() {
|
||||
if (cockpit.location.path[0]) {
|
||||
if ("search_rec" in cockpit.location.options) {
|
||||
delete cockpit.location.options.search_rec;
|
||||
}
|
||||
cockpit.location.go([], cockpit.location.options);
|
||||
} else {
|
||||
cockpit.location.go('/');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const r = this.props.recording;
|
||||
if (r == null) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<EmptyState variant={EmptyStateVariant.sm}>
|
||||
<Spinner />
|
||||
<EmptyStateHeader titleText={<>{_("Loading...")}</>} headingLevel="h2" />
|
||||
</EmptyState>
|
||||
</Bullseye>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Page
|
||||
groupProps={{ sticky: 'top' }}
|
||||
isBreadcrumbGrouped
|
||||
breadcrumb={
|
||||
<Breadcrumb className='machines-listing-breadcrumb'>
|
||||
<BreadcrumbItem to='#' onClick={this.handleGoBackToList}>
|
||||
{_("Session Recording")}
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem isActive>
|
||||
{_("Current recording")}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
}
|
||||
>
|
||||
<PageSection>
|
||||
<Player.Player
|
||||
ref={this.playerRef}
|
||||
matchList={this.props.recording.matchList}
|
||||
logsTs={this.logsTs}
|
||||
search={this.props.search}
|
||||
onTsChange={this.handleTsChange}
|
||||
recording={r}
|
||||
logsEnabled={this.state.logsEnabled}
|
||||
onRewindStart={this.handleLogsReset}
|
||||
/>
|
||||
<ExpandableSection
|
||||
id="btn-logs-view"
|
||||
toggleText={_("Logs View")}
|
||||
onToggle={this.handleLogsClick}
|
||||
isExpanded={this.state.logsEnabled === true}
|
||||
>
|
||||
<Logs
|
||||
recording={this.props.recording}
|
||||
curTs={this.state.curTs}
|
||||
onJumpToTs={this.handleLogTsChange}
|
||||
/>
|
||||
</ExpandableSection>
|
||||
</PageSection>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A component representing a list of recordings.
|
||||
* Properties:
|
||||
* - list: an array with recording objects, as created by the View below
|
||||
*/
|
||||
class RecordingList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleOnSort = this.handleOnSort.bind(this);
|
||||
this.handleRowClick = this.handleRowClick.bind(this);
|
||||
this.state = {
|
||||
sortBy: {
|
||||
index: 1,
|
||||
direction: SortByDirection.asc
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleOnSort(_event, index, direction) {
|
||||
this.setState({
|
||||
sortBy: {
|
||||
index,
|
||||
direction
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleRowClick(_event, row) {
|
||||
cockpit.location.go([row.id], cockpit.location.options);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { sortBy } = this.state;
|
||||
const { index, direction } = sortBy;
|
||||
|
||||
// generate columns
|
||||
const titles = ["User", "Start", "End", "Duration"];
|
||||
if (this.props.diff_hosts === true)
|
||||
titles.push("Hostname");
|
||||
const columnTitles = titles.map(title => ({
|
||||
title: _(title),
|
||||
transforms: [sortable]
|
||||
}));
|
||||
|
||||
// sort rows
|
||||
let rows = this.props.list.map(rec => {
|
||||
const cells = [
|
||||
rec.user,
|
||||
formatDateTime(rec.start),
|
||||
formatDateTime(rec.end),
|
||||
formatDuration(rec.end - rec.start),
|
||||
];
|
||||
if (this.props.diff_hosts === true)
|
||||
cells.push(rec.hostname);
|
||||
return {
|
||||
id: rec.id,
|
||||
cells,
|
||||
};
|
||||
}).sort((a, b) => a.cells[index].localeCompare(b.cells[index]));
|
||||
rows = direction === SortByDirection.asc ? rows : rows.reverse();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableDeprecated
|
||||
aria-label={_("Recordings")}
|
||||
cells={columnTitles}
|
||||
rows={rows}
|
||||
sortBy={sortBy}
|
||||
onSort={this.handleOnSort}
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody onRowClick={this.handleRowClick} />
|
||||
</TableDeprecated>
|
||||
{!rows.length &&
|
||||
<EmptyState variant={EmptyStateVariant.sm}>
|
||||
<EmptyStateHeader titleText={<>{_("No recordings found")}</>} icon={<EmptyStateIcon icon={SearchIcon} />} headingLevel="h2" />
|
||||
<EmptyStateBody>
|
||||
{_("No recordings matched the filter criteria.")}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A component representing the view upon a list of recordings, or a
|
||||
* single recording. Extracts the ID of the recording to display from
|
||||
* cockpit.location.path[0]. If it's zero, displays the list.
|
||||
*/
|
||||
export default class View extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onLocationChanged = this.onLocationChanged.bind(this);
|
||||
this.journalctlIngest = this.journalctlIngest.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleOpenConfig = this.handleOpenConfig.bind(this);
|
||||
/* Journalctl instance */
|
||||
this.journalctl = null;
|
||||
/* Recording ID journalctl instance is invoked with */
|
||||
this.journalctlRecordingID = null;
|
||||
/* Recording ID -> data map */
|
||||
this.recordingMap = {};
|
||||
const path = cockpit.location.path[0];
|
||||
this.state = {
|
||||
/* List of recordings in start order */
|
||||
recordingList: [],
|
||||
/* ID of the recording to display, or null for all */
|
||||
recordingID: path === "config" ? null : path || null,
|
||||
/* filter values start */
|
||||
date_since: cockpit.location.options.date_since || "",
|
||||
date_until: cockpit.location.options.date_until || "",
|
||||
username: cockpit.location.options.username || "",
|
||||
hostname: cockpit.location.options.hostname || "",
|
||||
search: cockpit.location.options.search || "",
|
||||
/* filter values end */
|
||||
error_tlog_user: false,
|
||||
diff_hosts: false,
|
||||
/* if config is open */
|
||||
config: path === "config",
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Display a journalctl error
|
||||
*/
|
||||
journalctlError(error) {
|
||||
console.warn(cockpit.message(error));
|
||||
}
|
||||
|
||||
/*
|
||||
* Respond to cockpit location change by extracting and setting the
|
||||
* displayed recording ID.
|
||||
*/
|
||||
onLocationChanged() {
|
||||
const path = cockpit.location.path[0];
|
||||
if (path === "config")
|
||||
this.setState({ config: true });
|
||||
else
|
||||
this.setState({
|
||||
recordingID: cockpit.location.path[0] || null,
|
||||
date_since: cockpit.location.options.date_since || "",
|
||||
date_until: cockpit.location.options.date_until || "",
|
||||
username: cockpit.location.options.username || "",
|
||||
hostname: cockpit.location.options.hostname || "",
|
||||
search: cockpit.location.options.search || "",
|
||||
config: false
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Ingest journal entries sent by journalctl.
|
||||
*/
|
||||
journalctlIngest(entryList) {
|
||||
const recordingList = this.state.recordingList.slice();
|
||||
let i;
|
||||
let j;
|
||||
let hostname;
|
||||
|
||||
if (entryList[0]) {
|
||||
if (entryList[0]._HOSTNAME) {
|
||||
hostname = entryList[0]._HOSTNAME;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < entryList.length; i++) {
|
||||
const e = entryList[i];
|
||||
const id = e.TLOG_REC;
|
||||
|
||||
/* Skip entries with missing recording ID */
|
||||
if (id === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ts = Math.floor(
|
||||
parseInt(e.__REALTIME_TIMESTAMP, 10) /
|
||||
1000);
|
||||
|
||||
let r = this.recordingMap[id];
|
||||
/* If no recording found */
|
||||
if (r === undefined) {
|
||||
/* Create new recording */
|
||||
if (hostname !== e._HOSTNAME) {
|
||||
this.setState({ diff_hosts: true });
|
||||
}
|
||||
|
||||
r = {
|
||||
id,
|
||||
matchList: ["TLOG_REC=" + id],
|
||||
user: e.TLOG_USER,
|
||||
boot_id: e._BOOT_ID,
|
||||
session_id: parseInt(e.TLOG_SESSION, 10),
|
||||
pid: parseInt(e._PID, 10),
|
||||
start: ts,
|
||||
/* FIXME Should be start + message duration */
|
||||
end: ts,
|
||||
hostname: e._HOSTNAME,
|
||||
duration: 0
|
||||
};
|
||||
/* Map the recording */
|
||||
this.recordingMap[id] = r;
|
||||
/* Insert the recording in order */
|
||||
for (j = recordingList.length - 1;
|
||||
j >= 0 && r.start < recordingList[j].start;
|
||||
j--);
|
||||
recordingList.splice(j + 1, 0, r);
|
||||
} else {
|
||||
/* Adjust existing recording */
|
||||
if (ts > r.end) {
|
||||
r.end = ts;
|
||||
r.duration = r.end - r.start;
|
||||
}
|
||||
if (ts < r.start) {
|
||||
r.start = ts;
|
||||
r.duration = r.end - r.start;
|
||||
/* Find the recording in the list */
|
||||
for (j = recordingList.length - 1;
|
||||
j >= 0 && recordingList[j] !== r;
|
||||
j--);
|
||||
/* If found */
|
||||
if (j >= 0) {
|
||||
/* Remove */
|
||||
recordingList.splice(j, 1);
|
||||
}
|
||||
/* Insert the recording in order */
|
||||
for (j = recordingList.length - 1;
|
||||
j >= 0 && r.start < recordingList[j].start;
|
||||
j--);
|
||||
recordingList.splice(j + 1, 0, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ recordingList });
|
||||
}
|
||||
|
||||
/*
|
||||
* Start journalctl, retrieving entries for the current recording ID.
|
||||
* Assumes journalctl is not running.
|
||||
*/
|
||||
journalctlStart() {
|
||||
const matches = ["_COMM=tlog-rec",
|
||||
/* Strings longer than TASK_COMM_LEN (16) characters
|
||||
* are truncated (man proc) */
|
||||
"_COMM=tlog-rec-sessio"];
|
||||
|
||||
if (this.state.username && this.state.username !== "") {
|
||||
matches.push("TLOG_USER=" + this.state.username);
|
||||
}
|
||||
if (this.state.hostname && this.state.hostname !== "") {
|
||||
matches.push("_HOSTNAME=" + this.state.hostname);
|
||||
}
|
||||
|
||||
const options = { follow: false, count: "all", merge: true };
|
||||
|
||||
if (this.state.date_since && this.state.date_since !== "") {
|
||||
options.since = formatUTC(this.state.date_since);
|
||||
}
|
||||
|
||||
if (this.state.date_until && this.state.date_until !== "") {
|
||||
options.until = formatUTC(this.state.date_until);
|
||||
}
|
||||
|
||||
if (this.state.search && this.state.search !== "" && this.state.recordingID === null) {
|
||||
options.grep = this.state.search;
|
||||
}
|
||||
|
||||
if (this.state.recordingID !== null) {
|
||||
delete options.grep;
|
||||
matches.push("TLOG_REC=" + this.state.recordingID);
|
||||
}
|
||||
|
||||
this.journalctlRecordingID = this.state.recordingID;
|
||||
this.journalctl = journal.journalctl(matches, options)
|
||||
.fail(this.journalctlError)
|
||||
.stream(this.journalctlIngest);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if journalctl is running.
|
||||
*/
|
||||
journalctlIsRunning() {
|
||||
return this.journalctl != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Stop current journalctl.
|
||||
* Assumes journalctl is running.
|
||||
*/
|
||||
journalctlStop() {
|
||||
this.journalctl.stop();
|
||||
this.journalctl = null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Restarts journalctl.
|
||||
* Will stop journalctl if it's running.
|
||||
*/
|
||||
journalctlRestart() {
|
||||
if (this.journalctlIsRunning()) {
|
||||
this.journalctl.stop();
|
||||
}
|
||||
this.journalctlStart();
|
||||
}
|
||||
|
||||
/*
|
||||
* Clears previous recordings list.
|
||||
* Will clear service obj recordingMap and state.
|
||||
*/
|
||||
clearRecordings() {
|
||||
this.recordingMap = {};
|
||||
this.setState({ recordingList: [] });
|
||||
}
|
||||
|
||||
throttleJournalRestart = debounce(300, () => {
|
||||
this.clearRecordings();
|
||||
this.journalctlRestart();
|
||||
});
|
||||
|
||||
handleInputChange(name, value) {
|
||||
const state = {};
|
||||
state[name] = value;
|
||||
this.setState(state);
|
||||
cockpit.location.go([], $.extend(cockpit.location.options, state));
|
||||
}
|
||||
|
||||
handleOpenConfig() {
|
||||
cockpit.location.go("/config");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const proc = cockpit.spawn(["getent", "passwd", "tlog"]);
|
||||
|
||||
proc.stream((data) => {
|
||||
this.journalctlStart();
|
||||
proc.close();
|
||||
});
|
||||
|
||||
proc.fail(() => {
|
||||
this.setState({ error_tlog_user: true });
|
||||
});
|
||||
|
||||
cockpit.addEventListener("locationchanged",
|
||||
this.onLocationChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.journalctlIsRunning()) {
|
||||
this.journalctlStop();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps, prevState) {
|
||||
/*
|
||||
* If we're running a specific (non-wildcard) journalctl
|
||||
* and recording ID has changed
|
||||
*/
|
||||
if (this.journalctlRecordingID !== null &&
|
||||
this.state.recordingID !== prevState.recordingID) {
|
||||
if (this.journalctlIsRunning()) {
|
||||
this.journalctlStop();
|
||||
}
|
||||
this.journalctlStart();
|
||||
}
|
||||
if (this.state.date_since !== prevState.date_since ||
|
||||
this.state.date_until !== prevState.date_until ||
|
||||
this.state.username !== prevState.username ||
|
||||
this.state.hostname !== prevState.hostname ||
|
||||
this.state.search !== prevState.search
|
||||
) {
|
||||
this.throttleJournalRestart();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.config === true) {
|
||||
return <Config.Config />;
|
||||
} else if (this.state.error_tlog_user === true) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<EmptyState variant={EmptyStateVariant.sm}>
|
||||
<EmptyStateHeader
|
||||
titleText={<>{_("Error")}</>}
|
||||
icon={
|
||||
<EmptyStateIcon
|
||||
icon={ExclamationCircleIcon}
|
||||
color={global_danger_color_200.value}
|
||||
/>
|
||||
} headingLevel="h2"
|
||||
/>
|
||||
<EmptyStateBody>
|
||||
{_("Unable to retrieve tlog user from system.")}
|
||||
</EmptyStateBody>
|
||||
</EmptyState>
|
||||
</Bullseye>
|
||||
);
|
||||
} else if (this.state.recordingID === null) {
|
||||
const toolbar = (
|
||||
<ToolbarContent>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem variant="label">{_("Since")}</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<TextInput
|
||||
id="filter-since"
|
||||
placeholder={_("Filter since")}
|
||||
value={this.state.date_since}
|
||||
type="search"
|
||||
onChange={(_event, value) => this.handleInputChange("date_since", value)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem variant="label">{_("Until")}</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<TextInput
|
||||
id="filter-until"
|
||||
placeholder={_("Filter until")}
|
||||
value={this.state.date_until}
|
||||
type="search"
|
||||
onChange={(_event, value) => this.handleInputChange("date_until", value)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem variant="label">{_("Search")}</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<TextInput
|
||||
id="filter-search"
|
||||
placeholder={_("Filter by content")}
|
||||
value={this.state.search}
|
||||
type="search"
|
||||
onChange={(_event, value) => this.handleInputChange("search", value)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem variant="label">{_("Username")}</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<TextInput
|
||||
id="filter-username"
|
||||
placeholder={_("Filter by username")}
|
||||
value={this.state.username}
|
||||
type="search"
|
||||
onChange={(_event, value) => this.handleInputChange("username", value)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
{this.state.diff_hosts === true &&
|
||||
<ToolbarGroup>
|
||||
<ToolbarItem variant="label">{_("Hostname")}</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<TextInput
|
||||
id="filter-hostname"
|
||||
placeholder={_("Filter by hostname")}
|
||||
value={this.state.hostname}
|
||||
type="search"
|
||||
onChange={(_event, value) => this.handleInputChange("hostname", value)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>}
|
||||
<ToolbarItem>
|
||||
<Button id="btn-config" onClick={this.handleOpenConfig}>
|
||||
<CogIcon />
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<Toolbar>{toolbar}</Toolbar>
|
||||
<RecordingList
|
||||
date_since={this.state.date_since}
|
||||
date_until={this.state.date_until}
|
||||
username={this.state.username}
|
||||
hostname={this.state.hostname}
|
||||
list={this.state.recordingList}
|
||||
diff_hosts={this.state.diff_hosts}
|
||||
/>
|
||||
</PageSection>
|
||||
</Page>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Recording
|
||||
recording={this.recordingMap[this.state.recordingID]}
|
||||
search={this.state.search}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
522
src/term.css
Normal file
522
src/term.css
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
.term-bg-color-0 { background-color: #2e3436; }
|
||||
.term-fg-color-0 { color: #2e3436; }
|
||||
.term-bg-color-1 { background-color: #cc0000; }
|
||||
.term-fg-color-1 { color: #cc0000; }
|
||||
.term-bg-color-2 { background-color: #4e9a06; }
|
||||
.term-fg-color-2 { color: #4e9a06; }
|
||||
.term-bg-color-3 { background-color: #c4a000; }
|
||||
.term-fg-color-3 { color: #c4a000; }
|
||||
.term-bg-color-4 { background-color: #3465a4; }
|
||||
.term-fg-color-4 { color: #3465a4; }
|
||||
.term-bg-color-5 { background-color: #75507b; }
|
||||
.term-fg-color-5 { color: #75507b; }
|
||||
.term-bg-color-6 { background-color: #06989a; }
|
||||
.term-fg-color-6 { color: #06989a; }
|
||||
.term-bg-color-7 { background-color: #d3d7cf; }
|
||||
.term-fg-color-7 { color: #d3d7cf; }
|
||||
.term-bg-color-8 { background-color: #555753; }
|
||||
.term-fg-color-8 { color: #555753; }
|
||||
.term-bg-color-9 { background-color: #ef2929; }
|
||||
.term-fg-color-9 { color: #ef2929; }
|
||||
.term-bg-color-10 { background-color: #8ae234; }
|
||||
.term-fg-color-10 { color: #8ae234; }
|
||||
.term-bg-color-11 { background-color: #fce94f; }
|
||||
.term-fg-color-11 { color: #fce94f; }
|
||||
.term-bg-color-12 { background-color: #729fcf; }
|
||||
.term-fg-color-12 { color: #729fcf; }
|
||||
.term-bg-color-13 { background-color: #ad7fa8; }
|
||||
.term-fg-color-13 { color: #ad7fa8; }
|
||||
.term-bg-color-14 { background-color: #34e2e2; }
|
||||
.term-fg-color-14 { color: #34e2e2; }
|
||||
.term-bg-color-15 { background-color: #eeeeec; }
|
||||
.term-fg-color-15 { color: #eeeeec; }
|
||||
.term-bg-color-16 { background-color: #000000; }
|
||||
.term-fg-color-16 { color: #000000; }
|
||||
.term-bg-color-17 { background-color: #00005f; }
|
||||
.term-fg-color-17 { color: #00005f; }
|
||||
.term-bg-color-18 { background-color: #000087; }
|
||||
.term-fg-color-18 { color: #000087; }
|
||||
.term-bg-color-19 { background-color: #0000af; }
|
||||
.term-fg-color-19 { color: #0000af; }
|
||||
.term-bg-color-20 { background-color: #0000d7; }
|
||||
.term-fg-color-20 { color: #0000d7; }
|
||||
.term-bg-color-21 { background-color: #0000ff; }
|
||||
.term-fg-color-21 { color: #0000ff; }
|
||||
.term-bg-color-22 { background-color: #005f00; }
|
||||
.term-fg-color-22 { color: #005f00; }
|
||||
.term-bg-color-23 { background-color: #005f5f; }
|
||||
.term-fg-color-23 { color: #005f5f; }
|
||||
.term-bg-color-24 { background-color: #005f87; }
|
||||
.term-fg-color-24 { color: #005f87; }
|
||||
.term-bg-color-25 { background-color: #005faf; }
|
||||
.term-fg-color-25 { color: #005faf; }
|
||||
.term-bg-color-26 { background-color: #005fd7; }
|
||||
.term-fg-color-26 { color: #005fd7; }
|
||||
.term-bg-color-27 { background-color: #005fff; }
|
||||
.term-fg-color-27 { color: #005fff; }
|
||||
.term-bg-color-28 { background-color: #008700; }
|
||||
.term-fg-color-28 { color: #008700; }
|
||||
.term-bg-color-29 { background-color: #00875f; }
|
||||
.term-fg-color-29 { color: #00875f; }
|
||||
.term-bg-color-30 { background-color: #008787; }
|
||||
.term-fg-color-30 { color: #008787; }
|
||||
.term-bg-color-31 { background-color: #0087af; }
|
||||
.term-fg-color-31 { color: #0087af; }
|
||||
.term-bg-color-32 { background-color: #0087d7; }
|
||||
.term-fg-color-32 { color: #0087d7; }
|
||||
.term-bg-color-33 { background-color: #0087ff; }
|
||||
.term-fg-color-33 { color: #0087ff; }
|
||||
.term-bg-color-34 { background-color: #00af00; }
|
||||
.term-fg-color-34 { color: #00af00; }
|
||||
.term-bg-color-35 { background-color: #00af5f; }
|
||||
.term-fg-color-35 { color: #00af5f; }
|
||||
.term-bg-color-36 { background-color: #00af87; }
|
||||
.term-fg-color-36 { color: #00af87; }
|
||||
.term-bg-color-37 { background-color: #00afaf; }
|
||||
.term-fg-color-37 { color: #00afaf; }
|
||||
.term-bg-color-38 { background-color: #00afd7; }
|
||||
.term-fg-color-38 { color: #00afd7; }
|
||||
.term-bg-color-39 { background-color: #00afff; }
|
||||
.term-fg-color-39 { color: #00afff; }
|
||||
.term-bg-color-40 { background-color: #00d700; }
|
||||
.term-fg-color-40 { color: #00d700; }
|
||||
.term-bg-color-41 { background-color: #00d75f; }
|
||||
.term-fg-color-41 { color: #00d75f; }
|
||||
.term-bg-color-42 { background-color: #00d787; }
|
||||
.term-fg-color-42 { color: #00d787; }
|
||||
.term-bg-color-43 { background-color: #00d7af; }
|
||||
.term-fg-color-43 { color: #00d7af; }
|
||||
.term-bg-color-44 { background-color: #00d7d7; }
|
||||
.term-fg-color-44 { color: #00d7d7; }
|
||||
.term-bg-color-45 { background-color: #00d7ff; }
|
||||
.term-fg-color-45 { color: #00d7ff; }
|
||||
.term-bg-color-46 { background-color: #00ff00; }
|
||||
.term-fg-color-46 { color: #00ff00; }
|
||||
.term-bg-color-47 { background-color: #00ff5f; }
|
||||
.term-fg-color-47 { color: #00ff5f; }
|
||||
.term-bg-color-48 { background-color: #00ff87; }
|
||||
.term-fg-color-48 { color: #00ff87; }
|
||||
.term-bg-color-49 { background-color: #00ffaf; }
|
||||
.term-fg-color-49 { color: #00ffaf; }
|
||||
.term-bg-color-50 { background-color: #00ffd7; }
|
||||
.term-fg-color-50 { color: #00ffd7; }
|
||||
.term-bg-color-51 { background-color: #00ffff; }
|
||||
.term-fg-color-51 { color: #00ffff; }
|
||||
.term-bg-color-52 { background-color: #5f0000; }
|
||||
.term-fg-color-52 { color: #5f0000; }
|
||||
.term-bg-color-53 { background-color: #5f005f; }
|
||||
.term-fg-color-53 { color: #5f005f; }
|
||||
.term-bg-color-54 { background-color: #5f0087; }
|
||||
.term-fg-color-54 { color: #5f0087; }
|
||||
.term-bg-color-55 { background-color: #5f00af; }
|
||||
.term-fg-color-55 { color: #5f00af; }
|
||||
.term-bg-color-56 { background-color: #5f00d7; }
|
||||
.term-fg-color-56 { color: #5f00d7; }
|
||||
.term-bg-color-57 { background-color: #5f00ff; }
|
||||
.term-fg-color-57 { color: #5f00ff; }
|
||||
.term-bg-color-58 { background-color: #5f5f00; }
|
||||
.term-fg-color-58 { color: #5f5f00; }
|
||||
.term-bg-color-59 { background-color: #5f5f5f; }
|
||||
.term-fg-color-59 { color: #5f5f5f; }
|
||||
.term-bg-color-60 { background-color: #5f5f87; }
|
||||
.term-fg-color-60 { color: #5f5f87; }
|
||||
.term-bg-color-61 { background-color: #5f5faf; }
|
||||
.term-fg-color-61 { color: #5f5faf; }
|
||||
.term-bg-color-62 { background-color: #5f5fd7; }
|
||||
.term-fg-color-62 { color: #5f5fd7; }
|
||||
.term-bg-color-63 { background-color: #5f5fff; }
|
||||
.term-fg-color-63 { color: #5f5fff; }
|
||||
.term-bg-color-64 { background-color: #5f8700; }
|
||||
.term-fg-color-64 { color: #5f8700; }
|
||||
.term-bg-color-65 { background-color: #5f875f; }
|
||||
.term-fg-color-65 { color: #5f875f; }
|
||||
.term-bg-color-66 { background-color: #5f8787; }
|
||||
.term-fg-color-66 { color: #5f8787; }
|
||||
.term-bg-color-67 { background-color: #5f87af; }
|
||||
.term-fg-color-67 { color: #5f87af; }
|
||||
.term-bg-color-68 { background-color: #5f87d7; }
|
||||
.term-fg-color-68 { color: #5f87d7; }
|
||||
.term-bg-color-69 { background-color: #5f87ff; }
|
||||
.term-fg-color-69 { color: #5f87ff; }
|
||||
.term-bg-color-70 { background-color: #5faf00; }
|
||||
.term-fg-color-70 { color: #5faf00; }
|
||||
.term-bg-color-71 { background-color: #5faf5f; }
|
||||
.term-fg-color-71 { color: #5faf5f; }
|
||||
.term-bg-color-72 { background-color: #5faf87; }
|
||||
.term-fg-color-72 { color: #5faf87; }
|
||||
.term-bg-color-73 { background-color: #5fafaf; }
|
||||
.term-fg-color-73 { color: #5fafaf; }
|
||||
.term-bg-color-74 { background-color: #5fafd7; }
|
||||
.term-fg-color-74 { color: #5fafd7; }
|
||||
.term-bg-color-75 { background-color: #5fafff; }
|
||||
.term-fg-color-75 { color: #5fafff; }
|
||||
.term-bg-color-76 { background-color: #5fd700; }
|
||||
.term-fg-color-76 { color: #5fd700; }
|
||||
.term-bg-color-77 { background-color: #5fd75f; }
|
||||
.term-fg-color-77 { color: #5fd75f; }
|
||||
.term-bg-color-78 { background-color: #5fd787; }
|
||||
.term-fg-color-78 { color: #5fd787; }
|
||||
.term-bg-color-79 { background-color: #5fd7af; }
|
||||
.term-fg-color-79 { color: #5fd7af; }
|
||||
.term-bg-color-80 { background-color: #5fd7d7; }
|
||||
.term-fg-color-80 { color: #5fd7d7; }
|
||||
.term-bg-color-81 { background-color: #5fd7ff; }
|
||||
.term-fg-color-81 { color: #5fd7ff; }
|
||||
.term-bg-color-82 { background-color: #5fff00; }
|
||||
.term-fg-color-82 { color: #5fff00; }
|
||||
.term-bg-color-83 { background-color: #5fff5f; }
|
||||
.term-fg-color-83 { color: #5fff5f; }
|
||||
.term-bg-color-84 { background-color: #5fff87; }
|
||||
.term-fg-color-84 { color: #5fff87; }
|
||||
.term-bg-color-85 { background-color: #5fffaf; }
|
||||
.term-fg-color-85 { color: #5fffaf; }
|
||||
.term-bg-color-86 { background-color: #5fffd7; }
|
||||
.term-fg-color-86 { color: #5fffd7; }
|
||||
.term-bg-color-87 { background-color: #5fffff; }
|
||||
.term-fg-color-87 { color: #5fffff; }
|
||||
.term-bg-color-88 { background-color: #870000; }
|
||||
.term-fg-color-88 { color: #870000; }
|
||||
.term-bg-color-89 { background-color: #87005f; }
|
||||
.term-fg-color-89 { color: #87005f; }
|
||||
.term-bg-color-90 { background-color: #870087; }
|
||||
.term-fg-color-90 { color: #870087; }
|
||||
.term-bg-color-91 { background-color: #8700af; }
|
||||
.term-fg-color-91 { color: #8700af; }
|
||||
.term-bg-color-92 { background-color: #8700d7; }
|
||||
.term-fg-color-92 { color: #8700d7; }
|
||||
.term-bg-color-93 { background-color: #8700ff; }
|
||||
.term-fg-color-93 { color: #8700ff; }
|
||||
.term-bg-color-94 { background-color: #875f00; }
|
||||
.term-fg-color-94 { color: #875f00; }
|
||||
.term-bg-color-95 { background-color: #875f5f; }
|
||||
.term-fg-color-95 { color: #875f5f; }
|
||||
.term-bg-color-96 { background-color: #875f87; }
|
||||
.term-fg-color-96 { color: #875f87; }
|
||||
.term-bg-color-97 { background-color: #875faf; }
|
||||
.term-fg-color-97 { color: #875faf; }
|
||||
.term-bg-color-98 { background-color: #875fd7; }
|
||||
.term-fg-color-98 { color: #875fd7; }
|
||||
.term-bg-color-99 { background-color: #875fff; }
|
||||
.term-fg-color-99 { color: #875fff; }
|
||||
.term-bg-color-100 { background-color: #878700; }
|
||||
.term-fg-color-100 { color: #878700; }
|
||||
.term-bg-color-101 { background-color: #87875f; }
|
||||
.term-fg-color-101 { color: #87875f; }
|
||||
.term-bg-color-102 { background-color: #878787; }
|
||||
.term-fg-color-102 { color: #878787; }
|
||||
.term-bg-color-103 { background-color: #8787af; }
|
||||
.term-fg-color-103 { color: #8787af; }
|
||||
.term-bg-color-104 { background-color: #8787d7; }
|
||||
.term-fg-color-104 { color: #8787d7; }
|
||||
.term-bg-color-105 { background-color: #8787ff; }
|
||||
.term-fg-color-105 { color: #8787ff; }
|
||||
.term-bg-color-106 { background-color: #87af00; }
|
||||
.term-fg-color-106 { color: #87af00; }
|
||||
.term-bg-color-107 { background-color: #87af5f; }
|
||||
.term-fg-color-107 { color: #87af5f; }
|
||||
.term-bg-color-108 { background-color: #87af87; }
|
||||
.term-fg-color-108 { color: #87af87; }
|
||||
.term-bg-color-109 { background-color: #87afaf; }
|
||||
.term-fg-color-109 { color: #87afaf; }
|
||||
.term-bg-color-110 { background-color: #87afd7; }
|
||||
.term-fg-color-110 { color: #87afd7; }
|
||||
.term-bg-color-111 { background-color: #87afff; }
|
||||
.term-fg-color-111 { color: #87afff; }
|
||||
.term-bg-color-112 { background-color: #87d700; }
|
||||
.term-fg-color-112 { color: #87d700; }
|
||||
.term-bg-color-113 { background-color: #87d75f; }
|
||||
.term-fg-color-113 { color: #87d75f; }
|
||||
.term-bg-color-114 { background-color: #87d787; }
|
||||
.term-fg-color-114 { color: #87d787; }
|
||||
.term-bg-color-115 { background-color: #87d7af; }
|
||||
.term-fg-color-115 { color: #87d7af; }
|
||||
.term-bg-color-116 { background-color: #87d7d7; }
|
||||
.term-fg-color-116 { color: #87d7d7; }
|
||||
.term-bg-color-117 { background-color: #87d7ff; }
|
||||
.term-fg-color-117 { color: #87d7ff; }
|
||||
.term-bg-color-118 { background-color: #87ff00; }
|
||||
.term-fg-color-118 { color: #87ff00; }
|
||||
.term-bg-color-119 { background-color: #87ff5f; }
|
||||
.term-fg-color-119 { color: #87ff5f; }
|
||||
.term-bg-color-120 { background-color: #87ff87; }
|
||||
.term-fg-color-120 { color: #87ff87; }
|
||||
.term-bg-color-121 { background-color: #87ffaf; }
|
||||
.term-fg-color-121 { color: #87ffaf; }
|
||||
.term-bg-color-122 { background-color: #87ffd7; }
|
||||
.term-fg-color-122 { color: #87ffd7; }
|
||||
.term-bg-color-123 { background-color: #87ffff; }
|
||||
.term-fg-color-123 { color: #87ffff; }
|
||||
.term-bg-color-124 { background-color: #af0000; }
|
||||
.term-fg-color-124 { color: #af0000; }
|
||||
.term-bg-color-125 { background-color: #af005f; }
|
||||
.term-fg-color-125 { color: #af005f; }
|
||||
.term-bg-color-126 { background-color: #af0087; }
|
||||
.term-fg-color-126 { color: #af0087; }
|
||||
.term-bg-color-127 { background-color: #af00af; }
|
||||
.term-fg-color-127 { color: #af00af; }
|
||||
.term-bg-color-128 { background-color: #af00d7; }
|
||||
.term-fg-color-128 { color: #af00d7; }
|
||||
.term-bg-color-129 { background-color: #af00ff; }
|
||||
.term-fg-color-129 { color: #af00ff; }
|
||||
.term-bg-color-130 { background-color: #af5f00; }
|
||||
.term-fg-color-130 { color: #af5f00; }
|
||||
.term-bg-color-131 { background-color: #af5f5f; }
|
||||
.term-fg-color-131 { color: #af5f5f; }
|
||||
.term-bg-color-132 { background-color: #af5f87; }
|
||||
.term-fg-color-132 { color: #af5f87; }
|
||||
.term-bg-color-133 { background-color: #af5faf; }
|
||||
.term-fg-color-133 { color: #af5faf; }
|
||||
.term-bg-color-134 { background-color: #af5fd7; }
|
||||
.term-fg-color-134 { color: #af5fd7; }
|
||||
.term-bg-color-135 { background-color: #af5fff; }
|
||||
.term-fg-color-135 { color: #af5fff; }
|
||||
.term-bg-color-136 { background-color: #af8700; }
|
||||
.term-fg-color-136 { color: #af8700; }
|
||||
.term-bg-color-137 { background-color: #af875f; }
|
||||
.term-fg-color-137 { color: #af875f; }
|
||||
.term-bg-color-138 { background-color: #af8787; }
|
||||
.term-fg-color-138 { color: #af8787; }
|
||||
.term-bg-color-139 { background-color: #af87af; }
|
||||
.term-fg-color-139 { color: #af87af; }
|
||||
.term-bg-color-140 { background-color: #af87d7; }
|
||||
.term-fg-color-140 { color: #af87d7; }
|
||||
.term-bg-color-141 { background-color: #af87ff; }
|
||||
.term-fg-color-141 { color: #af87ff; }
|
||||
.term-bg-color-142 { background-color: #afaf00; }
|
||||
.term-fg-color-142 { color: #afaf00; }
|
||||
.term-bg-color-143 { background-color: #afaf5f; }
|
||||
.term-fg-color-143 { color: #afaf5f; }
|
||||
.term-bg-color-144 { background-color: #afaf87; }
|
||||
.term-fg-color-144 { color: #afaf87; }
|
||||
.term-bg-color-145 { background-color: #afafaf; }
|
||||
.term-fg-color-145 { color: #afafaf; }
|
||||
.term-bg-color-146 { background-color: #afafd7; }
|
||||
.term-fg-color-146 { color: #afafd7; }
|
||||
.term-bg-color-147 { background-color: #afafff; }
|
||||
.term-fg-color-147 { color: #afafff; }
|
||||
.term-bg-color-148 { background-color: #afd700; }
|
||||
.term-fg-color-148 { color: #afd700; }
|
||||
.term-bg-color-149 { background-color: #afd75f; }
|
||||
.term-fg-color-149 { color: #afd75f; }
|
||||
.term-bg-color-150 { background-color: #afd787; }
|
||||
.term-fg-color-150 { color: #afd787; }
|
||||
.term-bg-color-151 { background-color: #afd7af; }
|
||||
.term-fg-color-151 { color: #afd7af; }
|
||||
.term-bg-color-152 { background-color: #afd7d7; }
|
||||
.term-fg-color-152 { color: #afd7d7; }
|
||||
.term-bg-color-153 { background-color: #afd7ff; }
|
||||
.term-fg-color-153 { color: #afd7ff; }
|
||||
.term-bg-color-154 { background-color: #afff00; }
|
||||
.term-fg-color-154 { color: #afff00; }
|
||||
.term-bg-color-155 { background-color: #afff5f; }
|
||||
.term-fg-color-155 { color: #afff5f; }
|
||||
.term-bg-color-156 { background-color: #afff87; }
|
||||
.term-fg-color-156 { color: #afff87; }
|
||||
.term-bg-color-157 { background-color: #afffaf; }
|
||||
.term-fg-color-157 { color: #afffaf; }
|
||||
.term-bg-color-158 { background-color: #afffd7; }
|
||||
.term-fg-color-158 { color: #afffd7; }
|
||||
.term-bg-color-159 { background-color: #afffff; }
|
||||
.term-fg-color-159 { color: #afffff; }
|
||||
.term-bg-color-160 { background-color: #d70000; }
|
||||
.term-fg-color-160 { color: #d70000; }
|
||||
.term-bg-color-161 { background-color: #d7005f; }
|
||||
.term-fg-color-161 { color: #d7005f; }
|
||||
.term-bg-color-162 { background-color: #d70087; }
|
||||
.term-fg-color-162 { color: #d70087; }
|
||||
.term-bg-color-163 { background-color: #d700af; }
|
||||
.term-fg-color-163 { color: #d700af; }
|
||||
.term-bg-color-164 { background-color: #d700d7; }
|
||||
.term-fg-color-164 { color: #d700d7; }
|
||||
.term-bg-color-165 { background-color: #d700ff; }
|
||||
.term-fg-color-165 { color: #d700ff; }
|
||||
.term-bg-color-166 { background-color: #d75f00; }
|
||||
.term-fg-color-166 { color: #d75f00; }
|
||||
.term-bg-color-167 { background-color: #d75f5f; }
|
||||
.term-fg-color-167 { color: #d75f5f; }
|
||||
.term-bg-color-168 { background-color: #d75f87; }
|
||||
.term-fg-color-168 { color: #d75f87; }
|
||||
.term-bg-color-169 { background-color: #d75faf; }
|
||||
.term-fg-color-169 { color: #d75faf; }
|
||||
.term-bg-color-170 { background-color: #d75fd7; }
|
||||
.term-fg-color-170 { color: #d75fd7; }
|
||||
.term-bg-color-171 { background-color: #d75fff; }
|
||||
.term-fg-color-171 { color: #d75fff; }
|
||||
.term-bg-color-172 { background-color: #d78700; }
|
||||
.term-fg-color-172 { color: #d78700; }
|
||||
.term-bg-color-173 { background-color: #d7875f; }
|
||||
.term-fg-color-173 { color: #d7875f; }
|
||||
.term-bg-color-174 { background-color: #d78787; }
|
||||
.term-fg-color-174 { color: #d78787; }
|
||||
.term-bg-color-175 { background-color: #d787af; }
|
||||
.term-fg-color-175 { color: #d787af; }
|
||||
.term-bg-color-176 { background-color: #d787d7; }
|
||||
.term-fg-color-176 { color: #d787d7; }
|
||||
.term-bg-color-177 { background-color: #d787ff; }
|
||||
.term-fg-color-177 { color: #d787ff; }
|
||||
.term-bg-color-178 { background-color: #d7af00; }
|
||||
.term-fg-color-178 { color: #d7af00; }
|
||||
.term-bg-color-179 { background-color: #d7af5f; }
|
||||
.term-fg-color-179 { color: #d7af5f; }
|
||||
.term-bg-color-180 { background-color: #d7af87; }
|
||||
.term-fg-color-180 { color: #d7af87; }
|
||||
.term-bg-color-181 { background-color: #d7afaf; }
|
||||
.term-fg-color-181 { color: #d7afaf; }
|
||||
.term-bg-color-182 { background-color: #d7afd7; }
|
||||
.term-fg-color-182 { color: #d7afd7; }
|
||||
.term-bg-color-183 { background-color: #d7afff; }
|
||||
.term-fg-color-183 { color: #d7afff; }
|
||||
.term-bg-color-184 { background-color: #d7d700; }
|
||||
.term-fg-color-184 { color: #d7d700; }
|
||||
.term-bg-color-185 { background-color: #d7d75f; }
|
||||
.term-fg-color-185 { color: #d7d75f; }
|
||||
.term-bg-color-186 { background-color: #d7d787; }
|
||||
.term-fg-color-186 { color: #d7d787; }
|
||||
.term-bg-color-187 { background-color: #d7d7af; }
|
||||
.term-fg-color-187 { color: #d7d7af; }
|
||||
.term-bg-color-188 { background-color: #d7d7d7; }
|
||||
.term-fg-color-188 { color: #d7d7d7; }
|
||||
.term-bg-color-189 { background-color: #d7d7ff; }
|
||||
.term-fg-color-189 { color: #d7d7ff; }
|
||||
.term-bg-color-190 { background-color: #d7ff00; }
|
||||
.term-fg-color-190 { color: #d7ff00; }
|
||||
.term-bg-color-191 { background-color: #d7ff5f; }
|
||||
.term-fg-color-191 { color: #d7ff5f; }
|
||||
.term-bg-color-192 { background-color: #d7ff87; }
|
||||
.term-fg-color-192 { color: #d7ff87; }
|
||||
.term-bg-color-193 { background-color: #d7ffaf; }
|
||||
.term-fg-color-193 { color: #d7ffaf; }
|
||||
.term-bg-color-194 { background-color: #d7ffd7; }
|
||||
.term-fg-color-194 { color: #d7ffd7; }
|
||||
.term-bg-color-195 { background-color: #d7ffff; }
|
||||
.term-fg-color-195 { color: #d7ffff; }
|
||||
.term-bg-color-196 { background-color: #ff0000; }
|
||||
.term-fg-color-196 { color: #ff0000; }
|
||||
.term-bg-color-197 { background-color: #ff005f; }
|
||||
.term-fg-color-197 { color: #ff005f; }
|
||||
.term-bg-color-198 { background-color: #ff0087; }
|
||||
.term-fg-color-198 { color: #ff0087; }
|
||||
.term-bg-color-199 { background-color: #ff00af; }
|
||||
.term-fg-color-199 { color: #ff00af; }
|
||||
.term-bg-color-200 { background-color: #ff00d7; }
|
||||
.term-fg-color-200 { color: #ff00d7; }
|
||||
.term-bg-color-201 { background-color: #ff00ff; }
|
||||
.term-fg-color-201 { color: #ff00ff; }
|
||||
.term-bg-color-202 { background-color: #ff5f00; }
|
||||
.term-fg-color-202 { color: #ff5f00; }
|
||||
.term-bg-color-203 { background-color: #ff5f5f; }
|
||||
.term-fg-color-203 { color: #ff5f5f; }
|
||||
.term-bg-color-204 { background-color: #ff5f87; }
|
||||
.term-fg-color-204 { color: #ff5f87; }
|
||||
.term-bg-color-205 { background-color: #ff5faf; }
|
||||
.term-fg-color-205 { color: #ff5faf; }
|
||||
.term-bg-color-206 { background-color: #ff5fd7; }
|
||||
.term-fg-color-206 { color: #ff5fd7; }
|
||||
.term-bg-color-207 { background-color: #ff5fff; }
|
||||
.term-fg-color-207 { color: #ff5fff; }
|
||||
.term-bg-color-208 { background-color: #ff8700; }
|
||||
.term-fg-color-208 { color: #ff8700; }
|
||||
.term-bg-color-209 { background-color: #ff875f; }
|
||||
.term-fg-color-209 { color: #ff875f; }
|
||||
.term-bg-color-210 { background-color: #ff8787; }
|
||||
.term-fg-color-210 { color: #ff8787; }
|
||||
.term-bg-color-211 { background-color: #ff87af; }
|
||||
.term-fg-color-211 { color: #ff87af; }
|
||||
.term-bg-color-212 { background-color: #ff87d7; }
|
||||
.term-fg-color-212 { color: #ff87d7; }
|
||||
.term-bg-color-213 { background-color: #ff87ff; }
|
||||
.term-fg-color-213 { color: #ff87ff; }
|
||||
.term-bg-color-214 { background-color: #ffaf00; }
|
||||
.term-fg-color-214 { color: #ffaf00; }
|
||||
.term-bg-color-215 { background-color: #ffaf5f; }
|
||||
.term-fg-color-215 { color: #ffaf5f; }
|
||||
.term-bg-color-216 { background-color: #ffaf87; }
|
||||
.term-fg-color-216 { color: #ffaf87; }
|
||||
.term-bg-color-217 { background-color: #ffafaf; }
|
||||
.term-fg-color-217 { color: #ffafaf; }
|
||||
.term-bg-color-218 { background-color: #ffafd7; }
|
||||
.term-fg-color-218 { color: #ffafd7; }
|
||||
.term-bg-color-219 { background-color: #ffafff; }
|
||||
.term-fg-color-219 { color: #ffafff; }
|
||||
.term-bg-color-220 { background-color: #ffd700; }
|
||||
.term-fg-color-220 { color: #ffd700; }
|
||||
.term-bg-color-221 { background-color: #ffd75f; }
|
||||
.term-fg-color-221 { color: #ffd75f; }
|
||||
.term-bg-color-222 { background-color: #ffd787; }
|
||||
.term-fg-color-222 { color: #ffd787; }
|
||||
.term-bg-color-223 { background-color: #ffd7af; }
|
||||
.term-fg-color-223 { color: #ffd7af; }
|
||||
.term-bg-color-224 { background-color: #ffd7d7; }
|
||||
.term-fg-color-224 { color: #ffd7d7; }
|
||||
.term-bg-color-225 { background-color: #ffd7ff; }
|
||||
.term-fg-color-225 { color: #ffd7ff; }
|
||||
.term-bg-color-226 { background-color: #ffff00; }
|
||||
.term-fg-color-226 { color: #ffff00; }
|
||||
.term-bg-color-227 { background-color: #ffff5f; }
|
||||
.term-fg-color-227 { color: #ffff5f; }
|
||||
.term-bg-color-228 { background-color: #ffff87; }
|
||||
.term-fg-color-228 { color: #ffff87; }
|
||||
.term-bg-color-229 { background-color: #ffffaf; }
|
||||
.term-fg-color-229 { color: #ffffaf; }
|
||||
.term-bg-color-230 { background-color: #ffffd7; }
|
||||
.term-fg-color-230 { color: #ffffd7; }
|
||||
.term-bg-color-231 { background-color: #ffffff; }
|
||||
.term-fg-color-231 { color: #ffffff; }
|
||||
.term-bg-color-232 { background-color: #080808; }
|
||||
.term-fg-color-232 { color: #080808; }
|
||||
.term-bg-color-233 { background-color: #121212; }
|
||||
.term-fg-color-233 { color: #121212; }
|
||||
.term-bg-color-234 { background-color: #1c1c1c; }
|
||||
.term-fg-color-234 { color: #1c1c1c; }
|
||||
.term-bg-color-235 { background-color: #262626; }
|
||||
.term-fg-color-235 { color: #262626; }
|
||||
.term-bg-color-236 { background-color: #303030; }
|
||||
.term-fg-color-236 { color: #303030; }
|
||||
.term-bg-color-237 { background-color: #3a3a3a; }
|
||||
.term-fg-color-237 { color: #3a3a3a; }
|
||||
.term-bg-color-238 { background-color: #444444; }
|
||||
.term-fg-color-238 { color: #444444; }
|
||||
.term-bg-color-239 { background-color: #4e4e4e; }
|
||||
.term-fg-color-239 { color: #4e4e4e; }
|
||||
.term-bg-color-240 { background-color: #585858; }
|
||||
.term-fg-color-240 { color: #585858; }
|
||||
.term-bg-color-241 { background-color: #626262; }
|
||||
.term-fg-color-241 { color: #626262; }
|
||||
.term-bg-color-242 { background-color: #6c6c6c; }
|
||||
.term-fg-color-242 { color: #6c6c6c; }
|
||||
.term-bg-color-243 { background-color: #767676; }
|
||||
.term-fg-color-243 { color: #767676; }
|
||||
.term-bg-color-244 { background-color: #808080; }
|
||||
.term-fg-color-244 { color: #808080; }
|
||||
.term-bg-color-245 { background-color: #8a8a8a; }
|
||||
.term-fg-color-245 { color: #8a8a8a; }
|
||||
.term-bg-color-246 { background-color: #949494; }
|
||||
.term-fg-color-246 { color: #949494; }
|
||||
.term-bg-color-247 { background-color: #9e9e9e; }
|
||||
.term-fg-color-247 { color: #9e9e9e; }
|
||||
.term-bg-color-248 { background-color: #a8a8a8; }
|
||||
.term-fg-color-248 { color: #a8a8a8; }
|
||||
.term-bg-color-249 { background-color: #b2b2b2; }
|
||||
.term-fg-color-249 { color: #b2b2b2; }
|
||||
.term-bg-color-250 { background-color: #bcbcbc; }
|
||||
.term-fg-color-250 { color: #bcbcbc; }
|
||||
.term-bg-color-251 { background-color: #c6c6c6; }
|
||||
.term-fg-color-251 { color: #c6c6c6; }
|
||||
.term-bg-color-252 { background-color: #d0d0d0; }
|
||||
.term-fg-color-252 { color: #d0d0d0; }
|
||||
.term-bg-color-253 { background-color: #dadada; }
|
||||
.term-fg-color-253 { color: #dadada; }
|
||||
.term-bg-color-254 { background-color: #e4e4e4; }
|
||||
.term-fg-color-254 { color: #e4e4e4; }
|
||||
.term-bg-color-255 { background-color: #eeeeee; }
|
||||
.term-fg-color-255 { color: #eeeeee; }
|
||||
.term-bg-color-default { background-color: #000000; }
|
||||
.term-bg-color-256 { background-color: #000000; }
|
||||
.term-fg-color-256 { color: #000000; }
|
||||
.term-fg-color-default { color: #f0f0f0; }
|
||||
.term-bg-color-257 { background-color: #f0f0f0; }
|
||||
.term-fg-color-257 { color: #f0f0f0; }
|
||||
.term-bold { font-weight: bold; }
|
||||
.term-underline { text-decoration: underline; }
|
||||
.term-blink { text-decoration: blink; }
|
||||
.term-hidden { visibility: hidden; }
|
||||
163
src/timer.css
Normal file
163
src/timer.css
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2015 Red Hat, Inc.
|
||||
*
|
||||
* Cockpit is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Cockpit is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#create-timer {
|
||||
display: none;
|
||||
}
|
||||
.vertical-scroll {
|
||||
max-height: 150px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.position-colon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div#boot {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
div#boot-or-specific-time {
|
||||
width: 170px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div#drop-time {
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input#boot-time {
|
||||
width: 50px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.hr, .min {
|
||||
width:30px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
background: #f4f4f4;
|
||||
border-width: 0 1px 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: #bababa;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#boot-label {
|
||||
position: relative;
|
||||
right: 8px;
|
||||
white-space: nowrap;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
#repeat-time .form-inline:first-of-type {
|
||||
border-top: 1px solid #bababa;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="month-days"] {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="week-days"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="close"] {
|
||||
position: relative;
|
||||
float: right;
|
||||
right: 8px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="add"] {
|
||||
position: relative;
|
||||
float: right;
|
||||
right: 4px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#repeat-time [data-provide="datepicker"] {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
[data-content='day-error'].repeat-error {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #4d5258;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.has-error {
|
||||
border-color: #cc0000;
|
||||
}
|
||||
|
||||
.has-error:hover {
|
||||
border-color: #990000;
|
||||
}
|
||||
|
||||
.has-error:focus {
|
||||
border-color: #990000;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ff3333;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ff3333;
|
||||
}
|
||||
|
||||
.repeat-error {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #cc0000;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
#services-page .datepicker-dropdown .prev,
|
||||
#services-page .datepicker-dropdown .next {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.date {
|
||||
width:120px;
|
||||
}
|
||||
|
||||
#hr-error, #min-error {
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
position: relative;
|
||||
bottom: 6px;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.cockpit-timer-modal-md {
|
||||
width: 500px;
|
||||
}
|
||||
.form-inline .form-control {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.form-inline .date .bootstrap-datepicker {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
66
test/browser/browser.sh
Executable file
66
test/browser/browser.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/bin/sh
|
||||
set -eux
|
||||
|
||||
export TEST_BROWSER=${TEST_BROWSER:-firefox}
|
||||
export TESTS="$(realpath $(dirname "$0"))"
|
||||
export SOURCE="$(realpath $TESTS/../..)"
|
||||
export FILES="$(realpath $TESTS/../files)"
|
||||
export LOGS="$(pwd)/logs"
|
||||
|
||||
mkdir -p "$LOGS"
|
||||
chmod a+w "$LOGS"
|
||||
|
||||
# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2033020
|
||||
dnf update -y pam || true
|
||||
|
||||
# install firefox (available everywhere in Fedora and RHEL)
|
||||
# we don't need the H.264 codec, and it is sometimes not available (rhbz#2005760)
|
||||
dnf install --disablerepo=fedora-cisco-openh264 -y --setopt=install_weak_deps=False firefox
|
||||
|
||||
# nodejs 10 is too old for current Cockpit test API
|
||||
if grep -q platform:el8 /etc/os-release; then
|
||||
dnf module switch-to -y nodejs:16
|
||||
fi
|
||||
|
||||
# create user account for logging in
|
||||
if ! id admin 2>/dev/null; then
|
||||
useradd -c Administrator -G wheel admin
|
||||
echo admin:foobar | chpasswd
|
||||
fi
|
||||
|
||||
# set root's password
|
||||
echo root:foobar | chpasswd
|
||||
|
||||
# avoid sudo lecture during tests
|
||||
su -c 'echo foobar | sudo --stdin whoami' - admin
|
||||
|
||||
# create user account for running the test
|
||||
if ! id runtest 2>/dev/null; then
|
||||
useradd -c 'Test runner' runtest
|
||||
# allow test to set up things on the machine
|
||||
mkdir -p /root/.ssh
|
||||
curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
fi
|
||||
chown -R runtest "$SOURCE"
|
||||
|
||||
# disable core dumps, we rather investigate them upstream where test VMs are accessible
|
||||
echo core > /proc/sys/kernel/core_pattern
|
||||
|
||||
## CSR specific setup ##
|
||||
# install cockpit-packagekit and glibc-langpack-en for testAppMenu
|
||||
dnf install -y cockpit-packagekit glibc-langpack-en
|
||||
|
||||
mkdir -p /var/log/journal/
|
||||
cp $FILES/1.journal /var/log/journal/1.journal
|
||||
cp $FILES/binary-rec.journal /var/log/journal/binary-rec.journal
|
||||
|
||||
systemctl enable --now cockpit.socket
|
||||
|
||||
# Run tests as unprivileged user
|
||||
# once we drop support for RHEL 8, use this:
|
||||
# runuser -u runtest --whitelist-environment=TEST_BROWSER,TEST_ALLOW_JOURNAL_MESSAGES,TEST_AUDIT_NO_SELINUX,SOURCE,LOGS $TESTS/run-test.sh
|
||||
runuser -u runtest --preserve-environment env USER=runtest HOME=$(getent passwd runtest | cut -f6 -d:) $TESTS/run-test.sh
|
||||
|
||||
RC=$(cat $LOGS/exitcode)
|
||||
exit ${RC:-1}
|
||||
14
test/browser/main.fmf
Normal file
14
test/browser/main.fmf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
require:
|
||||
- cockpit-session-recording
|
||||
- tlog
|
||||
- cockpit-ws
|
||||
- cockpit-system
|
||||
- cockpit-packagekit
|
||||
- bzip2
|
||||
- git-core
|
||||
- libvirt-python3
|
||||
- make
|
||||
- nodejs
|
||||
- python3
|
||||
test: ./browser.sh
|
||||
duration: 30m
|
||||
42
test/browser/run-test.sh
Executable file
42
test/browser/run-test.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/bin/sh
|
||||
set -eux
|
||||
|
||||
# tests need cockpit's bots/ libraries and test infrastructure
|
||||
cd $SOURCE
|
||||
git init
|
||||
rm -f bots # common local case: existing bots symlink
|
||||
make bots test/common
|
||||
|
||||
# support running from clean git tree
|
||||
if [ ! -d node_modules/chrome-remote-interface ]; then
|
||||
# copy package.json temporarily otherwise npm might try to install the dependencies from it
|
||||
rm -f package-lock.json # otherwise the command below installs *everything*, argh
|
||||
mv package.json .package.json
|
||||
# only install a subset to save time/space
|
||||
npm install chrome-remote-interface sizzle
|
||||
mv .package.json package.json
|
||||
fi
|
||||
|
||||
# disable detection of affected tests; testing takes too long as there is no parallelization
|
||||
mv .git dot-git
|
||||
|
||||
. /etc/os-release
|
||||
export TEST_OS="${ID}-${VERSION_ID/./-}"
|
||||
|
||||
if [ "${TEST_OS#centos-}" != "$TEST_OS" ]; then
|
||||
TEST_OS="${TEST_OS}-stream"
|
||||
fi
|
||||
|
||||
EXCLUDES=""
|
||||
|
||||
# make it easy to check in logs
|
||||
echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}"
|
||||
echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}"
|
||||
|
||||
RC=0
|
||||
test/common/run-tests --nondestructive --machine 127.0.0.1:22 --browser 127.0.0.1:9090 $EXCLUDES || RC=$?
|
||||
|
||||
echo $RC > "$LOGS/exitcode"
|
||||
cp --verbose Test* "$LOGS" || true
|
||||
# deliver test result via exitcode file
|
||||
exit 0
|
||||
|
|
@ -1,51 +1,412 @@
|
|||
#!/usr/bin/python3
|
||||
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
|
||||
|
||||
# Run this with --help to see available options for tracing and debugging
|
||||
# See https://github.com/cockpit-project/cockpit/blob/master/test/common/testlib.py
|
||||
# "class Browser" and "class MachineCase" for the available API.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# import Cockpit's machinery for test VMs and its browser test API
|
||||
TEST_DIR = os.path.dirname(__file__)
|
||||
sys.path.append(os.path.join(TEST_DIR, "common"))
|
||||
sys.path.append(os.path.join(os.path.dirname(TEST_DIR), "bots/machine"))
|
||||
import testlib
|
||||
|
||||
import time
|
||||
import json
|
||||
import configparser
|
||||
|
||||
# Nondestructive tests all run in the same running VM. This allows them to run in Packit, Fedora, and RHEL dist-git gating
|
||||
# They must not permanently change any file or configuration on the system in a way that influences other tests.
|
||||
@testlib.nondestructive
|
||||
class TestApplication(testlib.MachineCase):
|
||||
def testBasic(self):
|
||||
def _login(self, loc="/session-recording", wait="#app"):
|
||||
self.login_and_go(loc)
|
||||
b = self.browser
|
||||
m = self.machine
|
||||
b.wait_visible(wait)
|
||||
self.allow_journal_messages('.*type=1400.*avc: denied .* comm="systemctl".*')
|
||||
self.allow_journal_messages('.*invalid non-UTF8.*web_socket_connection_send.*')
|
||||
self.allow_journal_messages('.*Locale charset.*ANSI.*')
|
||||
self.allow_journal_messages('.*Assuming locale environment.*UTF-8.*')
|
||||
return b, m
|
||||
|
||||
self.login_and_go("/starter-kit")
|
||||
# verify expected heading
|
||||
b.wait_present(".container-fluid h2")
|
||||
b.wait_text(".container-fluid h2", "Starter Kit")
|
||||
def _sel_rec(self, recording):
|
||||
'''
|
||||
rec1:
|
||||
whoami
|
||||
id
|
||||
echo thisisatest123
|
||||
sleep 16
|
||||
echo thisisanothertest456
|
||||
exit
|
||||
|
||||
# verify expected host name
|
||||
hostname = m.execute("hostname").strip()
|
||||
b.wait_present(".container-fluid p")
|
||||
b.wait_text(".container-fluid p", "Running on " + hostname)
|
||||
rec2:
|
||||
echo "Extra Commands"
|
||||
sudo systemctl daemon-reload
|
||||
sudo ssh root@localhost
|
||||
exit
|
||||
|
||||
# change language to German
|
||||
b.switch_to_top()
|
||||
b.click("#content-user-name")
|
||||
b.click(".display-language-menu a")
|
||||
b.wait_popup('display-language')
|
||||
b.set_val("#display-language select", "de-de")
|
||||
b.click("#display-language-select-button")
|
||||
b.expect_load()
|
||||
# HACK: work around language switching in Chrome not working in current session (Cockpit issue #8160)
|
||||
b.reload(ignore_cache=True)
|
||||
b.wait_present("#content")
|
||||
# menu label (from manifest) should be translated
|
||||
b.wait_text("#host-apps a[href='/starter-kit']", "Bausatz")
|
||||
binaryrec:
|
||||
mc
|
||||
exit
|
||||
'''
|
||||
recordings = {'rec1': '0f25700a28c44b599869745e5fda8b0c-7106-121e79',
|
||||
'rec2': '0f25700a28c44b599869745e5fda8b0c-7623-135541',
|
||||
'binaryrec': '976e4ef1d66741848ed35f7600b94c5c-1a0f-c1ae'}
|
||||
|
||||
b.go("/starter-kit")
|
||||
b.enter_page("/starter-kit")
|
||||
# page label (from js) should be translated
|
||||
b.wait_in_text(".container-fluid p", "Läuft auf")
|
||||
page = recordings[recording]
|
||||
|
||||
if __name__ == '__main__':
|
||||
self.browser.go(f"/session-recording#/{page}")
|
||||
|
||||
def _term_line(self, lineno):
|
||||
return f".xterm-accessibility-tree div:nth-child({lineno})"
|
||||
|
||||
def testPlay(self):
|
||||
b, _ = self._login()
|
||||
self._sel_rec('rec1')
|
||||
b.click("#player-play-pause")
|
||||
b.wait_in_text(self._term_line(1), "localhost")
|
||||
|
||||
def testPlayBinary(self):
|
||||
b, _ = self._login()
|
||||
self._sel_rec('binaryrec')
|
||||
b.click("#player-play-pause")
|
||||
time.sleep(5)
|
||||
b.wait_in_text(self._term_line(4), "exit")
|
||||
|
||||
def testFastforwardControls(self):
|
||||
progress = ".pf-v5-c-progress__indicator"
|
||||
|
||||
b, _ = self._login()
|
||||
self._sel_rec('rec1')
|
||||
# fast forward
|
||||
b.click("#player-fast-forward")
|
||||
b.wait_in_text(self._term_line(12), "exit")
|
||||
b.wait_attr(progress, "style", "width: 100%;")
|
||||
# test restart playback
|
||||
b.click("#player-restart")
|
||||
b.click("#player-play-pause")
|
||||
b.wait_text(self._term_line(7), "thisisatest123")
|
||||
with b.wait_timeout(100):
|
||||
b.wait_attr(progress, "style", "width: 100%;")
|
||||
|
||||
def testSpeedControls(self):
|
||||
b, _ = self._login()
|
||||
self._sel_rec('rec1')
|
||||
# increase speed
|
||||
b.wait_visible("#player-speed-up")
|
||||
b.click("#player-speed-up")
|
||||
b.wait_text("#player-speed", "x2")
|
||||
b.click("#player-speed-up")
|
||||
b.wait_text("#player-speed", "x4")
|
||||
b.click("#player-speed-up")
|
||||
b.wait_text("#player-speed", "x8")
|
||||
b.click("#player-speed-up")
|
||||
b.wait_text("#player-speed", "x16")
|
||||
# decrease speed
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "x8")
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "x4")
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "x2")
|
||||
b.click("#player-speed-down")
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "/2")
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "/4")
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "/8")
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "/16")
|
||||
# restore speed
|
||||
b.click(".pf-v5-c-chip .pf-v5-c-button")
|
||||
b.click("#player-speed-down")
|
||||
b.wait_text("#player-speed", "/2")
|
||||
|
||||
def testZoomControls(self):
|
||||
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
|
||||
zoom_one_scale_sel = '.console-ct[style^="transform: scale(1.1)"]'
|
||||
zoom_two_scale_sel = '.console-ct[style^="transform: scale(1.2)"]'
|
||||
zoom_three_scale_sel = '.console-ct[style^="transform: scale(1.3)"]'
|
||||
zoom_fit_to = (
|
||||
'.console-ct[style*="translate(-50%, -50%)"]'
|
||||
'[style*="top: 50%"]'
|
||||
'[style*="left: 50%"]'
|
||||
)
|
||||
|
||||
b, _ = self._login()
|
||||
self._sel_rec('rec1')
|
||||
# Wait for terminal with scale(1)
|
||||
b.wait_visible(default_scale_sel)
|
||||
# Zoom in x3
|
||||
b.click("#player-zoom-in")
|
||||
b.wait_visible(zoom_one_scale_sel)
|
||||
b.click("#player-zoom-in")
|
||||
b.wait_visible(zoom_two_scale_sel)
|
||||
b.click("#player-zoom-in")
|
||||
b.wait_visible(zoom_three_scale_sel)
|
||||
# Zoom Out
|
||||
b.click("#player-zoom-out")
|
||||
b.wait_visible(zoom_two_scale_sel)
|
||||
# Fit zoom to screen
|
||||
b.click("#player-fit-to")
|
||||
b.wait_visible(zoom_fit_to)
|
||||
|
||||
def testSkipFrame(self):
|
||||
b, _ = self._login()
|
||||
self._sel_rec('rec1')
|
||||
b.wait_visible(self._term_line(1))
|
||||
# loop until 3 valid frames have passed
|
||||
while "localhost" not in b.text(self._term_line(1)):
|
||||
b.click("#player-skip-frame")
|
||||
b.wait_in_text(self._term_line(1), "localhost")
|
||||
|
||||
def testPlaybackPause(self):
|
||||
b, _ = self._login()
|
||||
self._sel_rec('rec1')
|
||||
# Start and pause the player
|
||||
b.click("#player-restart")
|
||||
b.click("#player-play-pause")
|
||||
b.click("#player-play-pause")
|
||||
time.sleep(10)
|
||||
# Make sure it didn't keep playing
|
||||
b.wait_not_in_text(self._term_line(6), "thisisatest123")
|
||||
# Test if it can start playing again
|
||||
b.click("#player-play-pause")
|
||||
|
||||
def testSessionRecordingConf(self):
|
||||
b, m = self._login()
|
||||
b.click("#btn-config")
|
||||
|
||||
# TLOG config
|
||||
conf_file_path = "/etc/tlog/"
|
||||
conf_file = f"{conf_file_path}tlog-rec-session.conf"
|
||||
save_file = "/tmp/tlog-rec-session.conf"
|
||||
test_file = "/tmp/test-tlog-rec-session.conf"
|
||||
|
||||
# Save the existing config
|
||||
b.click("#btn-save-tlog-conf")
|
||||
m.download(conf_file, save_file)
|
||||
# Change all of the fields
|
||||
b.set_input_text("#shell", "/test/path/shell")
|
||||
b.set_input_text("#notice", "Test Notice")
|
||||
b.set_input_text("#latency", "1")
|
||||
b.set_input_text("#payload", "2")
|
||||
b.set_checked("#log_input", True)
|
||||
b.set_checked("#log_output", False)
|
||||
b.set_checked("#log_window", False)
|
||||
b.set_input_text("#limit_rate", "3")
|
||||
b.set_input_text("#limit_burst", "4")
|
||||
b.set_val("#limit_action", "drop")
|
||||
b.set_input_text("#file_path", "/test/path/file")
|
||||
b.set_input_text("#syslog_facility", "testfac")
|
||||
b.set_val("#syslog_priority", "info")
|
||||
b.set_val("#journal_priority", "info")
|
||||
b.set_checked("#journal_augment", False)
|
||||
b.set_val("#writer", "file")
|
||||
b.click("#btn-save-tlog-conf")
|
||||
time.sleep(1)
|
||||
m.download(conf_file, test_file)
|
||||
# Revert to the previous config before testing to ensure test continuity
|
||||
m.upload([save_file], conf_file_path)
|
||||
# Check that the config reflects the changes
|
||||
conf = json.load(open(test_file, "r"))
|
||||
self.assertEqual(
|
||||
json.dumps(conf),
|
||||
json.dumps(
|
||||
{
|
||||
"shell": "/test/path/shell",
|
||||
"notice": "Test Notice",
|
||||
"latency": 1,
|
||||
"payload": 2,
|
||||
"log": {"input": True, "output": False, "window": False},
|
||||
"limit": {"rate": 3, "burst": 4, "action": "drop"},
|
||||
"file": {"path": "/test/path/file"},
|
||||
"syslog": {"facility": "testfac", "priority": "info"},
|
||||
"journal": {"priority": "info", "augment": False},
|
||||
"writer": "file",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
# SSSD config
|
||||
conf_file_path = "/etc/sssd/conf.d/"
|
||||
conf_file = f"{conf_file_path}sssd-session-recording.conf"
|
||||
save_file = "/tmp/sssd-session-recording.conf"
|
||||
test_none_file = "/tmp/test-none-sssd-session-recording.conf"
|
||||
test_some_file = "/tmp/test-some-sssd-session-recording.conf"
|
||||
test_all_file = "/tmp/test-all-sssd-session-recording.conf"
|
||||
|
||||
# Save the existing config
|
||||
b.click("#btn-save-sssd-conf")
|
||||
time.sleep(1)
|
||||
m.download(conf_file, save_file)
|
||||
# Download test with scope 'Some'
|
||||
b.set_val("#scope", "some")
|
||||
b.set_input_text("#users", "test users")
|
||||
b.set_input_text("#groups", "test groups")
|
||||
b.click("#btn-save-sssd-conf")
|
||||
time.sleep(1)
|
||||
m.download(conf_file, test_some_file)
|
||||
# Download test with scope 'All'
|
||||
b.set_val("#scope", "all")
|
||||
b.set_input_text("#exclude_users", "testuser1")
|
||||
b.set_input_text("#exclude_groups", "testgroup1")
|
||||
b.click("#btn-save-sssd-conf")
|
||||
time.sleep(1)
|
||||
m.download(conf_file, test_all_file)
|
||||
# Download test with scope 'None'
|
||||
b.set_val("#scope", "none")
|
||||
b.click("#btn-save-sssd-conf")
|
||||
time.sleep(1)
|
||||
m.download(conf_file, test_none_file)
|
||||
# Revert to the previous config before testing to ensure test continuity
|
||||
m.upload([save_file], conf_file_path)
|
||||
# Check that the configs reflected the changes
|
||||
conf = configparser.ConfigParser()
|
||||
conf.read_file(open(test_some_file, "r"))
|
||||
self.assertEqual(conf["session_recording"]["scope"], "some")
|
||||
self.assertEqual(conf["session_recording"]["users"], "test users")
|
||||
self.assertEqual(conf["session_recording"]["groups"], "test groups")
|
||||
conf.read_file(open(test_all_file, "r"))
|
||||
self.assertEqual(conf["session_recording"]["scope"], "all")
|
||||
self.assertEqual(conf["session_recording"]["exclude_users"], "testuser1")
|
||||
self.assertEqual(conf["session_recording"]["exclude_groups"], "testgroup1")
|
||||
self.assertEqual(conf["domain/nssfiles"]["id_provider"], "proxy")
|
||||
self.assertEqual(conf["domain/nssfiles"]["proxy_lib_name"], "files")
|
||||
self.assertEqual(conf["domain/nssfiles"]["proxy_pam_target"], "sssd-shadowutils")
|
||||
conf.read_file(open(test_none_file, "r"))
|
||||
self.assertEqual(conf["session_recording"]["scope"], "none")
|
||||
|
||||
def testDisplayDrag(self):
|
||||
b, _ = self._login()
|
||||
self._sel_rec('rec1')
|
||||
# start playback and pause in middle
|
||||
b.click("#player-play-pause")
|
||||
b.wait_in_text(self._term_line(1), "localhost")
|
||||
b.click("#player-play-pause")
|
||||
# zoom in so that the whole screen is no longer visible
|
||||
b.click("#player-zoom-in")
|
||||
b.click("#player-zoom-in")
|
||||
# select and ensure drag'n'pan mode
|
||||
b.click("#player-drag-pan")
|
||||
# scroll and check for screen movement
|
||||
b.mouse(".dragnpan", "mousedown", 200, 200)
|
||||
b.mouse(".dragnpan", "mousemove", 10, 10)
|
||||
self.assertNotEqual(b.attr(".dragnpan", "scrollTop"), 0)
|
||||
self.assertNotEqual(b.attr(".dragnpan", "scrollLeft"), 0)
|
||||
|
||||
def testLogCorrelation(self):
|
||||
b, m = self._login()
|
||||
# make sure system is on expected timezone EST
|
||||
m.execute("timedatectl set-timezone America/New_York")
|
||||
# select the recording with the extra logs
|
||||
self._sel_rec('rec2')
|
||||
b.click("#btn-logs-view .pf-v5-c-expandable-section__toggle")
|
||||
# fast forward until the end
|
||||
while "exit" not in b.text(self._term_line(22)):
|
||||
b.click("#player-skip-frame")
|
||||
# check for extra log entries
|
||||
b.wait_visible(".pf-v5-c-data-list:contains('authentication failure')")
|
||||
|
||||
def testZoomSpeedControls(self):
|
||||
b, m = self._login()
|
||||
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
|
||||
self._sel_rec('rec1')
|
||||
# set speed x16 and begin playing
|
||||
for _ in range(4):
|
||||
b.click("#player-speed-up")
|
||||
b.wait_visible(default_scale_sel)
|
||||
b.click("#player-play-pause")
|
||||
# wait until sleeping and zoom in
|
||||
b.wait_in_text(self._term_line(8), "sleep")
|
||||
b.click("#player-zoom-in")
|
||||
b.wait_not_present(default_scale_sel)
|
||||
# zoom out while typing fast
|
||||
b.wait_in_text(self._term_line(9), "localhost")
|
||||
b.click("#player-zoom-out")
|
||||
b.wait_not_present(default_scale_sel)
|
||||
|
||||
def _filter(self, inp, occ_dict):
|
||||
m = self.machine
|
||||
|
||||
m.execute("timedatectl set-timezone America/New_York")
|
||||
# ignore errors from half-entered timestamps due to searches occuring
|
||||
# before `set_input_text` is complete
|
||||
self.allow_journal_messages(".*timestamp.*")
|
||||
# login and test inputs
|
||||
b, _ = self._login()
|
||||
time.sleep(5)
|
||||
for occ in occ_dict:
|
||||
for term in occ_dict[occ]:
|
||||
# enter the search term and wait for the results to return
|
||||
b.set_input_text(inp, term)
|
||||
time.sleep(5)
|
||||
self.assertEqual(b.text(".pf-v5-c-table").count("contractor"), occ)
|
||||
|
||||
def testSearch(self):
|
||||
self._filter(
|
||||
"#filter-search",
|
||||
{
|
||||
0: {
|
||||
"this should return nothing",
|
||||
"this should also return nothing",
|
||||
"0123456789",
|
||||
},
|
||||
1: {
|
||||
"extra commands",
|
||||
"whoami",
|
||||
"ssh",
|
||||
"thisisatest123",
|
||||
"thisisanothertest456",
|
||||
},
|
||||
2: {
|
||||
"id",
|
||||
"localhost",
|
||||
"exit",
|
||||
"actor",
|
||||
"contractor",
|
||||
"contractor1@localhost",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def testFilterUsername(self):
|
||||
self._filter(
|
||||
"#filter-username",
|
||||
{
|
||||
0: {"test", "contact", "contractor", "contractor11", "contractor4"},
|
||||
2: {"contractor1"},
|
||||
},
|
||||
)
|
||||
|
||||
def testFilterSince(self):
|
||||
self._filter(
|
||||
"#filter-since",
|
||||
{
|
||||
0: {"2020-06-02", "2020-06-01 12:31:00"},
|
||||
1: {"2020-06-01 12:17:01", "2020-06-01 12:30:50"},
|
||||
2: {"2020-06-01", "2020-06-01 12:17:00"},
|
||||
},
|
||||
)
|
||||
|
||||
def testFilterUntil(self):
|
||||
self._filter(
|
||||
"#filter-until",
|
||||
{
|
||||
0: {"2020-06-01", "2020-06-01 12:16"},
|
||||
1: {"2020-06-01 12:17", "2020-06-01 12:29"},
|
||||
2: {"2020-06-02", "2020-06-01 12:31:00"},
|
||||
},
|
||||
)
|
||||
|
||||
def testAppMenu(self):
|
||||
srrow = ".app-list .pf-v5-c-data-list__item-row:" \
|
||||
"contains('Session Recording')"
|
||||
srbut = "{} button:contains('Session Recording')" \
|
||||
"".format(srrow)
|
||||
b, _ = self._login("/apps", srrow)
|
||||
self.allow_journal_messages(".*chromium-browser.appdata.xml.*",
|
||||
".*xml.etree.ElementTree.ParseError:.*")
|
||||
b.click(srbut)
|
||||
b.enter_page("/session-recording")
|
||||
b.wait_visible("#app")
|
||||
|
||||
if __name__ == "__main__":
|
||||
testlib.test_main()
|
||||
|
|
|
|||
BIN
test/files/1.journal
Normal file
BIN
test/files/1.journal
Normal file
Binary file not shown.
BIN
test/files/binary-rec.journal
Normal file
BIN
test/files/binary-rec.journal
Normal file
Binary file not shown.
1
test/reference-image
Normal file
1
test/reference-image
Normal file
|
|
@ -0,0 +1 @@
|
|||
fedora-36
|
||||
14
test/run
14
test/run
|
|
@ -1,4 +1,14 @@
|
|||
#! /bin/bash
|
||||
#! /bin/sh
|
||||
set -eu
|
||||
|
||||
# This is the expected entry point for Cockpit CI; will be called without
|
||||
# arguments but with an appropriate $TEST_OS
|
||||
# arguments but with an appropriate $TEST_OS, and optionally $TEST_SCENARIO
|
||||
|
||||
TEST_SCENARIO="${TEST_SCENARIO:-}"
|
||||
[ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox
|
||||
export RUN_TESTS_OPTIONS=--track-naughties
|
||||
|
||||
# linters are off by default for production builds, but we want to run them in CI
|
||||
export LINT=1
|
||||
|
||||
make check
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -10,3 +10,6 @@ if type firewall-cmd >/dev/null 2>&1; then
|
|||
firewall-cmd --add-service=cockpit --permanent
|
||||
fi
|
||||
systemctl enable cockpit.socket
|
||||
|
||||
# needed for testAppMenu
|
||||
dnf install -y cockpit-packagekit
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
const path = require("path");
|
||||
const copy = require("copy-webpack-plugin");
|
||||
const extract = require("extract-text-webpack-plugin");
|
||||
const fs = require("fs");
|
||||
const webpack = require("webpack");
|
||||
const CompressionPlugin = require("compression-webpack-plugin");
|
||||
|
||||
var externals = {
|
||||
"cockpit": "cockpit",
|
||||
};
|
||||
|
||||
/* These can be overridden, typically from the Makefile.am */
|
||||
const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src";
|
||||
const builddir = (process.env.SRCDIR || __dirname);
|
||||
const distdir = builddir + path.sep + "dist";
|
||||
const section = process.env.ONLYDIR || null;
|
||||
const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules");
|
||||
|
||||
/* A standard nodejs and webpack pattern */
|
||||
var production = process.env.NODE_ENV === 'production';
|
||||
|
||||
var info = {
|
||||
entries: {
|
||||
"index": [
|
||||
"./index.es6"
|
||||
]
|
||||
},
|
||||
files: [
|
||||
"index.html",
|
||||
"manifest.json",
|
||||
],
|
||||
};
|
||||
|
||||
var output = {
|
||||
path: distdir,
|
||||
filename: "[name].js",
|
||||
sourceMapFilename: "[file].map",
|
||||
};
|
||||
|
||||
/*
|
||||
* Note that we're avoiding the use of path.join as webpack and nodejs
|
||||
* want relative paths that start with ./ explicitly.
|
||||
*
|
||||
* In addition we mimic the VPATH style functionality of GNU Makefile
|
||||
* where we first check builddir, and then srcdir.
|
||||
*/
|
||||
|
||||
function vpath(/* ... */) {
|
||||
var filename = Array.prototype.join.call(arguments, path.sep);
|
||||
var expanded = builddir + path.sep + filename;
|
||||
if (fs.existsSync(expanded))
|
||||
return expanded;
|
||||
expanded = srcdir + path.sep + filename;
|
||||
return expanded;
|
||||
}
|
||||
|
||||
/* Qualify all the paths in entries */
|
||||
Object.keys(info.entries).forEach(function(key) {
|
||||
if (section && key.indexOf(section) !== 0) {
|
||||
delete info.entries[key];
|
||||
return;
|
||||
}
|
||||
|
||||
info.entries[key] = info.entries[key].map(function(value) {
|
||||
if (value.indexOf("/") === -1)
|
||||
return value;
|
||||
else
|
||||
return vpath(value);
|
||||
});
|
||||
});
|
||||
|
||||
/* Qualify all the paths in files listed */
|
||||
var files = [];
|
||||
info.files.forEach(function(value) {
|
||||
if (!section || value.indexOf(section) === 0)
|
||||
files.push({ from: vpath("src", value), to: value });
|
||||
});
|
||||
info.files = files;
|
||||
|
||||
var plugins = [
|
||||
new copy(info.files),
|
||||
new extract("[name].css")
|
||||
];
|
||||
|
||||
/* Only minimize when in production mode */
|
||||
if (production) {
|
||||
/* Rename output files when minimizing */
|
||||
output.filename = "[name].min.js";
|
||||
|
||||
plugins.unshift(new CompressionPlugin({
|
||||
asset: "[path].gz[query]",
|
||||
test: /\.(js|html)$/,
|
||||
minRatio: 0.9,
|
||||
deleteOriginalAssets: true
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mode: production ? 'production' : 'development',
|
||||
entry: info.entries,
|
||||
externals: externals,
|
||||
output: output,
|
||||
devtool: "source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader',
|
||||
test: /\.jsx$/
|
||||
},
|
||||
{
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader',
|
||||
test: /\.es6$/
|
||||
},
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
test: /\.js$/
|
||||
},
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
test: /\.jsx$/
|
||||
},
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
test: /\.es6$/
|
||||
},
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
loader: extract.extract('css-loader!sass-loader'),
|
||||
test: /\.scss$/
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: plugins
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue