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 |
44 changed files with 4722 additions and 744 deletions
22
.cirrus.yml
22
.cirrus.yml
|
|
@ -1,22 +0,0 @@
|
|||
container:
|
||||
# official cockpit CI container, with cockpit related build and test dependencies
|
||||
# if you want to use your own, see the documentation about required packages:
|
||||
# https://github.com/cockpit-project/cockpit/blob/main/HACKING.md#getting-the-development-dependencies
|
||||
image: ghcr.io/cockpit-project/tasks
|
||||
kvm: true
|
||||
# increase this if you have many tests that benefit from parallelism
|
||||
cpu: 1
|
||||
|
||||
test_task:
|
||||
env:
|
||||
matrix:
|
||||
- TEST_OS: fedora-42
|
||||
- TEST_OS: centos-9-stream
|
||||
|
||||
fix_kvm_script: sudo chmod 666 /dev/kvm
|
||||
|
||||
# test PO template generation
|
||||
pot_build_script: make po/starter-kit.pot
|
||||
|
||||
# chromium has too little /dev/shm, and we can't make that bigger
|
||||
check_script: TEST_BROWSER=firefox TEST_JOBS=$(nproc) TEST_OS=$TEST_OS make check
|
||||
|
|
@ -1 +0,0 @@
|
|||
ghcr.io/cockpit-project/tasks:2025-07-26
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"ecmaVersion": "2022",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "react-hooks"],
|
||||
"plugins": ["flowtype", "react", "react-hooks"],
|
||||
"rules": {
|
||||
"indent": ["error", 4,
|
||||
{
|
||||
|
|
@ -37,25 +37,12 @@
|
|||
"quotes": "off",
|
||||
"react/jsx-curly-spacing": "off",
|
||||
"react/jsx-indent-props": "off",
|
||||
"react/jsx-no-useless-fragment": "error",
|
||||
"react/prop-types": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"standard/no-callback-literal": "off"
|
||||
},
|
||||
"globals": {
|
||||
"require": "readonly",
|
||||
"module": "readonly"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.json"]
|
||||
}
|
||||
}]
|
||||
"require": false,
|
||||
"module": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
.flake8
2
.flake8
|
|
@ -1,2 +0,0 @@
|
|||
[flake8]
|
||||
max-line-length = 118
|
||||
51
.github/dependabot.yml
vendored
51
.github/dependabot.yml
vendored
|
|
@ -1,51 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# run these when most of our developers don't work, don't DoS our CI over the day
|
||||
time: "22:00"
|
||||
timezone: "Europe/Berlin"
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
eslint:
|
||||
patterns:
|
||||
- "eslint*"
|
||||
esbuild:
|
||||
patterns:
|
||||
- "esbuild*"
|
||||
patternfly:
|
||||
patterns:
|
||||
- "@patternfly*"
|
||||
react:
|
||||
patterns:
|
||||
- "react*"
|
||||
stylelint:
|
||||
patterns:
|
||||
- "stylelint*"
|
||||
types:
|
||||
patterns:
|
||||
- "@types*"
|
||||
- "types*"
|
||||
|
||||
ignore:
|
||||
# https://github.com/cockpit-project/cockpit/issues/21151
|
||||
- dependency-name: "sass"
|
||||
versions: [">=1.80.0", "2.x"]
|
||||
|
||||
# needs to be done in Cockpit first
|
||||
- dependency-name: "@patternfly/*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
||||
# PF5 requires React 18
|
||||
- dependency-name: "*react*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 3
|
||||
labels:
|
||||
- "no-test"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
30
.github/workflows/cockpit-lib-update.yml
vendored
30
.github/workflows/cockpit-lib-update.yml
vendored
|
|
@ -1,30 +0,0 @@
|
|||
name: cockpit-lib-update
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 4'
|
||||
# can be run manually on https://github.com/cockpit-project/starter-kit/actions
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
cockpit-lib-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Set up dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y make
|
||||
|
||||
- name: Set up configuration and secrets
|
||||
run: |
|
||||
printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig
|
||||
echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token
|
||||
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run cockpit-lib-update
|
||||
run: |
|
||||
make bots
|
||||
bots/cockpit-lib-update
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
# Create a GitHub upstream release. Replace "TARNAME" with your project tarball
|
||||
# name and enable this by dropping the ".disabled" suffix from the file name.
|
||||
# Create a GitHub upstream release
|
||||
# See README.md.
|
||||
name: release
|
||||
on:
|
||||
|
|
@ -11,20 +10,20 @@ jobs:
|
|||
source:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/cockpit-project/tasks:latest
|
||||
image: ghcr.io/cockpit-project/unit-tests
|
||||
options: --user root
|
||||
permissions:
|
||||
# create GitHub release
|
||||
contents: write
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
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/
|
||||
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)
|
||||
|
|
@ -33,6 +32,6 @@ jobs:
|
|||
run: make dist
|
||||
|
||||
- name: Publish GitHub release
|
||||
uses: cockpit-project/action-release@7d2e2657382e8d34f88a24b5987f2b81ea165785
|
||||
uses: cockpit-project/action-release@88d994da62d1451c7073e26748c18413fcdf46e9
|
||||
with:
|
||||
filename: "TARNAME-${{ github.ref_name }}.tar.xz"
|
||||
filename: "cockpit-session-recording-${{ github.ref_name }}.tar.xz"
|
||||
34
.github/workflows/tasks-container-update.yml
vendored
34
.github/workflows/tasks-container-update.yml
vendored
|
|
@ -1,34 +0,0 @@
|
|||
name: tasks-container-update
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 1'
|
||||
# can be run manually on https://github.com/cockpit-project/starter-kit/actions
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
tasks-container-update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
container:
|
||||
image: ghcr.io/cockpit-project/tasks
|
||||
options: --user root
|
||||
steps:
|
||||
- name: Set up configuration and secrets
|
||||
run: |
|
||||
printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig
|
||||
mkdir -p ~/.config
|
||||
echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token
|
||||
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
|
||||
- name: Pacify git's permission check
|
||||
run: git config --global --add safe.directory /__w/starter-kit/starter-kit
|
||||
|
||||
- name: Run tasks-container-update
|
||||
run: |
|
||||
make bots
|
||||
bots/tasks-container-update
|
||||
50
.gitignore
vendored
50
.gitignore
vendored
|
|
@ -1,35 +1,19 @@
|
|||
# Please keep this file sorted (LC_COLLATE=C.UTF-8),
|
||||
# grouped into the 3 categories below:
|
||||
# - general patterns (match in all directories)
|
||||
# - patterns to match files at the toplevel
|
||||
# - patterns to match files in subdirs
|
||||
|
||||
# general patterns
|
||||
*.pyc
|
||||
*~
|
||||
*.retry
|
||||
*.tar.xz
|
||||
*.rpm
|
||||
|
||||
# toplevel (/...)
|
||||
/Test*.html
|
||||
/Test*.json
|
||||
/Test*.log
|
||||
/Test*.log.gz
|
||||
/Test*.png
|
||||
/*.whl
|
||||
node_modules/
|
||||
dist/
|
||||
/*.spec
|
||||
/.vagrant
|
||||
package-lock.json
|
||||
Test*FAIL*
|
||||
/bots
|
||||
/cockpit-*.tar.xz
|
||||
/cockpit-navigator.spec
|
||||
/dist/
|
||||
/package-lock.json
|
||||
/pkg/
|
||||
/node_modules/
|
||||
/tmp/
|
||||
/tools/
|
||||
|
||||
# subdirs (/subdir/...)
|
||||
/packaging/arch/PKGBUILD
|
||||
/packaging/debian/changelog
|
||||
/po/*.pot
|
||||
/po/LINGUAS
|
||||
/test/common/
|
||||
/test/images/
|
||||
/test/static-code
|
||||
test/common/
|
||||
test/images/
|
||||
pkg
|
||||
*.pot
|
||||
POTFILES*
|
||||
tmp/
|
||||
/po/LINGUAS
|
||||
/tools
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"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,
|
||||
|
|
@ -15,13 +19,13 @@
|
|||
"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/load-partial-extension": null,
|
||||
"scss/at-import-no-partial-leading-underscore": null,
|
||||
"scss/load-no-partial-leading-underscore": true,
|
||||
"scss/at-import-partial-extension": null,
|
||||
"scss/at-mixin-pattern": null,
|
||||
"scss/comment-no-empty": null,
|
||||
"scss/dollar-variable-pattern": null,
|
||||
|
|
|
|||
36
Makefile
36
Makefile
|
|
@ -3,14 +3,14 @@ PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' packa
|
|||
RPM_NAME := cockpit-$(PACKAGE_NAME)
|
||||
VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.')
|
||||
ifeq ($(TEST_OS),)
|
||||
TEST_OS = centos-9-stream
|
||||
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.$(subst -,_,$(PACKAGE_NAME)).metainfo.xml
|
||||
APPSTREAMFILE=org.cockpit-project.$(PACKAGE_NAME).metainfo.xml
|
||||
VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS)
|
||||
# stamp file to check for node_modules/
|
||||
NODE_MODULES_TEST=package-lock.json
|
||||
|
|
@ -32,7 +32,7 @@ COCKPIT_REPO_FILES = \
|
|||
$(NULL)
|
||||
|
||||
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
|
||||
COCKPIT_REPO_COMMIT = 8076a6044ea41f378547d04e9f539a77f63191dc # 343 + 1 commits
|
||||
COCKPIT_REPO_COMMIT = 9c73bec7e1dc2395a00aa0c510fd7210b6c96a16 # 300.1 + 42 commits
|
||||
|
||||
$(COCKPIT_REPO_FILES): $(COCKPIT_REPO_STAMP)
|
||||
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
||||
|
|
@ -48,21 +48,19 @@ $(COCKPIT_REPO_STAMP): Makefile
|
|||
LINGUAS=$(basename $(notdir $(wildcard po/*.po)))
|
||||
|
||||
po/$(PACKAGE_NAME).js.pot:
|
||||
xgettext --default-domain=$(PACKAGE_NAME) --output=- --language=C --keyword= \
|
||||
--add-comments=Translators: \
|
||||
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ --language=C --keyword= \
|
||||
--keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
|
||||
--keyword=N_ --keyword=NC_:1c,2 \
|
||||
--keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
|
||||
--keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
|
||||
--keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
|
||||
--from-code=UTF-8 $$(find src/ -name '*.[jt]s' -o -name '*.[jt]sx') | \
|
||||
sed '/^#/ s/, c-format//' > $@
|
||||
--from-code=UTF-8 $$(find src/ -name '*.js' -o -name '*.jsx')
|
||||
|
||||
po/$(PACKAGE_NAME).html.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
|
||||
pkg/lib/html2po -o $@ $$(find src -name '*.html')
|
||||
pkg/lib/html2po.js -o $@ $$(find src -name '*.html')
|
||||
|
||||
po/$(PACKAGE_NAME).manifest.pot: $(COCKPIT_REPO_STAMP)
|
||||
pkg/lib/manifest2po -o $@ src/manifest.json
|
||||
po/$(PACKAGE_NAME).manifest.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
|
||||
pkg/lib/manifest2po.js src/manifest.json -o $@
|
||||
|
||||
po/$(PACKAGE_NAME).metainfo.pot: $(APPSTREAMFILE)
|
||||
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ $<
|
||||
|
|
@ -81,9 +79,6 @@ $(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' $< > $@
|
||||
|
||||
packaging/arch/PKGBUILD: packaging/arch/PKGBUILD.in
|
||||
sed 's/VERSION/$(VERSION)/; s/SOURCE/$(TARFILE)/' $< > $@
|
||||
|
||||
$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js
|
||||
NODE_ENV=$(NODE_ENV) ./build.js
|
||||
|
||||
|
|
@ -92,7 +87,7 @@ watch: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
|
|||
|
||||
clean:
|
||||
rm -rf dist/
|
||||
rm -f $(SPEC) packaging/arch/PKGBUILD
|
||||
rm -f $(SPEC)
|
||||
rm -f po/LINGUAS
|
||||
|
||||
install: $(DIST_TEST) po/LINGUAS
|
||||
|
|
@ -124,12 +119,11 @@ dist: $(TARFILE)
|
|||
# pre-built dist/ (so it's not necessary) and ship package-lock.json (so that
|
||||
# node_modules/ can be reconstructed if necessary)
|
||||
$(TARFILE): export NODE_ENV=production
|
||||
$(TARFILE): $(DIST_TEST) $(SPEC) packaging/arch/PKGBUILD
|
||||
$(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) packaging/arch/PKGBUILD dist/
|
||||
$$(git ls-files) $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) $(SPEC) dist/
|
||||
|
||||
$(NODE_CACHE): $(NODE_MODULES_TEST)
|
||||
tar --xz $(TAR_ARGS) -cf $@ node_modules
|
||||
|
|
@ -161,10 +155,11 @@ rpm: $(TARFILE) $(NODE_CACHE) $(SPEC)
|
|||
|
||||
# build a VM with locally built distro pkgs installed
|
||||
# disable networking, VM images have mock/pbuilder with the common build dependencies pre-installed
|
||||
$(VM_IMAGE): export XZ_OPT=-0
|
||||
$(VM_IMAGE): $(TARFILE) $(NODE_CACHE) bots test/vm.install
|
||||
bots/image-customize --no-network --fresh \
|
||||
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
|
||||
|
|
@ -184,9 +179,6 @@ prepare-check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common
|
|||
check: prepare-check
|
||||
test/common/run-tests ${RUN_TESTS_OPTIONS}
|
||||
|
||||
codecheck: test/common $(NODE_MODULES_TEST)
|
||||
test/common/static-code
|
||||
|
||||
# checkout Cockpit's bots for standard test VM images and API to launch them
|
||||
bots: $(COCKPIT_REPO_STAMP)
|
||||
test/common/make-bots
|
||||
|
|
|
|||
135
README.md
135
README.md
|
|
@ -1,25 +1,33 @@
|
|||
# Cockpit Starter Kit
|
||||
# Cockpit Session Recording
|
||||
|
||||
Scaffolding for a [Cockpit](https://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).
|
||||
|
||||
# Development dependencies
|
||||
Demos & Talks:
|
||||
|
||||
On Debian/Ubuntu:
|
||||
* [Demo 1 on YouTube](https://youtu.be/5-0WBf4rOrc)
|
||||
* [Demo 2 on YouTube](https://youtu.be/Fw8g_fFvwcs)
|
||||
* [FOSDEM talk](https://youtu.be/sHO5y28EHXg)
|
||||
|
||||
sudo apt install gettext nodejs npm make
|
||||
GitHub Organization:
|
||||
|
||||
On Fedora:
|
||||
|
||||
sudo dnf install gettext nodejs npm make
|
||||
* [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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
|
|
@ -39,7 +47,7 @@ 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
|
||||
|
|
@ -49,23 +57,23 @@ 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
|
||||
$ ./build.js -w
|
||||
|
||||
or
|
||||
|
||||
make watch
|
||||
$ 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
|
||||
$ 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
|
||||
$ RSYNC_DEVEL=example.com make watch
|
||||
|
||||
To "uninstall" the locally installed version, run `make devel-uninstall`, or
|
||||
remove manually the symlink:
|
||||
|
|
@ -75,17 +83,17 @@ remove manually the symlink:
|
|||
# Running eslint
|
||||
|
||||
Cockpit Starter Kit uses [ESLint](https://eslint.org/) to automatically check
|
||||
JavaScript/TypeScript code style in `.js[x]` and `.ts[x]` files.
|
||||
JavaScript code style in `.js` and `.jsx` files.
|
||||
|
||||
eslint is executed as part of `test/static-code`, aka. `make codecheck`.
|
||||
eslint is executed within every build.
|
||||
|
||||
For developer convenience, the ESLint can be started explicitly by:
|
||||
|
||||
npm run eslint
|
||||
$ npm run eslint
|
||||
|
||||
Violations of some rules can be fixed automatically by:
|
||||
|
||||
npm run eslint:fix
|
||||
$ npm run eslint:fix
|
||||
|
||||
Rules configuration can be found in the `.eslintrc.json` file.
|
||||
|
||||
|
|
@ -94,22 +102,28 @@ Rules configuration can be found in the `.eslintrc.json` file.
|
|||
Cockpit uses [Stylelint](https://stylelint.io/) to automatically check CSS code
|
||||
style in `.css` and `scss` files.
|
||||
|
||||
styleint is executed as part of `test/static-code`, aka. `make codecheck`.
|
||||
styleint is executed within every build.
|
||||
|
||||
For developer convenience, the Stylelint can be started explicitly by:
|
||||
|
||||
npm run stylelint
|
||||
$ npm run stylelint
|
||||
|
||||
Violations of some rules can be fixed automatically by:
|
||||
|
||||
npm run stylelint:fix
|
||||
$ 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-9-stream 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
|
||||
|
|
@ -120,81 +134,12 @@ 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-9-stream 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-9-stream make prepare-check
|
||||
TEST_OS=centos-8-stream make prepare-check
|
||||
|
||||
You can also run the test against a different Cockpit image, for example:
|
||||
|
||||
TEST_OS=fedora-40 make check
|
||||
|
||||
# Running tests in CI
|
||||
|
||||
These tests can be run in [Cirrus CI](https://cirrus-ci.org/), on their free
|
||||
[Linux Containers](https://cirrus-ci.org/guide/linux/) environment which
|
||||
explicitly supports `/dev/kvm`. Please see [Quick
|
||||
Start](https://cirrus-ci.org/guide/quick-start/) how to set up Cirrus CI for
|
||||
your project after forking from starter-kit.
|
||||
|
||||
The included [.cirrus.yml](./.cirrus.yml) runs the integration tests for two
|
||||
operating systems (Fedora and CentOS 8). Note that if/once your project grows
|
||||
bigger, or gets frequent changes, you may need to move to a paid account, or
|
||||
different infrastructure with more capacity.
|
||||
|
||||
Tests also run in [Packit](https://packit.dev/) for all currently supported
|
||||
Fedora releases; see the [packit.yaml](./packit.yaml) control file. You need to
|
||||
[enable Packit-as-a-service](https://packit.dev/docs/packit-service/) in your GitHub project to use this.
|
||||
To run the tests in the exact same way for upstream pull requests and for
|
||||
[Fedora package update gating](https://docs.fedoraproject.org/en-US/ci/), the
|
||||
tests are wrapped in the [FMF metadata format](https://github.com/teemtee/fmf)
|
||||
for using with the [tmt test management tool](https://docs.fedoraproject.org/en-US/ci/tmt/).
|
||||
Note that Packit tests can *not* run their own virtual machine images, thus
|
||||
they only run [@nondestructive tests](https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py).
|
||||
|
||||
# Customizing
|
||||
|
||||
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. The intention is that the only manual step for releasing a project is to create
|
||||
a signed tag for the version number, which includes a summary of the noteworthy
|
||||
changes:
|
||||
|
||||
```
|
||||
123
|
||||
|
||||
- this new feature
|
||||
- fix bug #123
|
||||
```
|
||||
|
||||
Pushing the release tag triggers the [release.yml](.github/workflows/release.yml.disabled)
|
||||
[GitHub action](https://github.com/features/actions) workflow. This creates the
|
||||
official release tarball and publishes as upstream release to GitHub. The
|
||||
workflow is disabled by default -- to use it, edit the file as per the comment
|
||||
at the top, and rename it to just `*.yml`.
|
||||
|
||||
The Fedora and COPR releases are done with [Packit](https://packit.dev/),
|
||||
see the [packit.yaml](./packit.yaml) control file.
|
||||
|
||||
# Automated maintenance
|
||||
|
||||
It is important to keep your [NPM modules](./package.json) up to date, to keep
|
||||
up with security updates and bug fixes. This happens with
|
||||
[dependabot](https://github.com/dependabot),
|
||||
see [configuration file](.github/dependabot.yml).
|
||||
|
||||
# Further reading
|
||||
|
||||
* The [Starter Kit announcement](https://cockpit-project.org/blog/cockpit-starter-kit.html)
|
||||
blog post explains the rationale for this project.
|
||||
* [Cockpit Deployment and Developer documentation](https://cockpit-project.org/guide/latest/)
|
||||
* [Make your project easily discoverable](https://cockpit-project.org/blog/making-a-cockpit-application.html)
|
||||
TEST_OS=fedora-34 make check
|
||||
|
|
|
|||
20
build.js
20
build.js
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import os from 'node:os';
|
||||
|
||||
import copy from 'esbuild-plugin-copy';
|
||||
|
|
@ -12,15 +11,19 @@ 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('-m', '--metafile', { help: "Enable bundle size information file", metavar: "FILE" });
|
||||
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)
|
||||
|
|
@ -52,12 +55,15 @@ function notifyEndPlugin() {
|
|||
};
|
||||
}
|
||||
|
||||
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
|
||||
if (ev !== "change" || fname.startsWith('.')) {
|
||||
const isHidden = /^\./.test(fname);
|
||||
if (ev !== "change" || isHidden) {
|
||||
return;
|
||||
}
|
||||
on_change(path.join(dir, fname));
|
||||
|
|
@ -83,13 +89,14 @@ const context = await esbuild.context({
|
|||
external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts
|
||||
legalComments: 'external', // Move all legal comments to a .LEGAL.txt file
|
||||
loader: { ".js": "jsx" },
|
||||
metafile: !!args.metafile,
|
||||
minify: production,
|
||||
nodePaths,
|
||||
outdir,
|
||||
target: ['es2020'],
|
||||
plugins: [
|
||||
cleanPlugin(),
|
||||
...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({
|
||||
|
|
@ -107,10 +114,7 @@ const context = await esbuild.context({
|
|||
});
|
||||
|
||||
try {
|
||||
const result = await context.rebuild();
|
||||
if (args.metafile) {
|
||||
fs.writeFileSync(args.metafile, JSON.stringify(result.metafile));
|
||||
}
|
||||
await context.rebuild();
|
||||
} catch (e) {
|
||||
if (!args.watch)
|
||||
process.exit(1);
|
||||
|
|
|
|||
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,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.
|
||||
|
||||
This is just a demo which does not do much. Please replace
|
||||
this with a real description.
|
||||
</p>
|
||||
</description>
|
||||
<extends>org.cockpit_project.cockpit</extends>
|
||||
<launchable type="cockpit-manifest">starter-kit</launchable>
|
||||
<url type="homepage">https://github.com/cockpit-project/starter-kit</url>
|
||||
<url type="bugtracker">https://github.com/cockpit-project/starter-kit/issues</url>
|
||||
<update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
|
||||
<developer id="org.cockpit-project">
|
||||
<name>Cockpit Project</name>
|
||||
</developer>
|
||||
</component>
|
||||
88
package.json
88
package.json
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "starter-kit",
|
||||
"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": {
|
||||
|
|
@ -12,48 +12,56 @@
|
|||
"scripts": {
|
||||
"watch": "ESBUILD_WATCH='true' ./build.js",
|
||||
"build": "./build.js",
|
||||
"eslint": "eslint src/",
|
||||
"eslint:fix": "eslint --fix src/",
|
||||
"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": {
|
||||
"@types/react": "18.3.13",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.38.0",
|
||||
"argparse": "2.0.1",
|
||||
"esbuild": "0.25.8",
|
||||
"esbuild-plugin-copy": "2.1.1",
|
||||
"esbuild-plugin-replace": "1.4.0",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"esbuild-wasm": "0.25.8",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-config-standard-jsx": "11.0.0",
|
||||
"eslint-config-standard-react": "13.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"gettext-parser": "8.0.0",
|
||||
"glob": "11.0.3",
|
||||
"jed": "1.1.1",
|
||||
"qunit": "2.24.1",
|
||||
"sass": "1.79.6",
|
||||
"stylelint": "16.22.0",
|
||||
"stylelint-config-recommended-scss": "15.0.1",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"stylelint-config-standard-scss": "15.0.1",
|
||||
"stylelint-formatter-pretty": "4.0.1",
|
||||
"typescript": "5.8.3"
|
||||
"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",
|
||||
"qunit": "^2.9.3",
|
||||
"sass": "^1.61.0",
|
||||
"sizzle": "^2.3.3",
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-config-standard-scss": "^10.0.0",
|
||||
"stylelint-formatter-pretty": "^3.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@patternfly/patternfly": "6.1.0",
|
||||
"@patternfly/react-core": "6.1.0",
|
||||
"@patternfly/react-icons": "6.1.0",
|
||||
"@patternfly/react-styles": "6.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
pkgname=cockpit-starter-kit
|
||||
pkgver=VERSION
|
||||
pkgrel=1
|
||||
pkgdesc='Cockpit Starter Kit Example Module'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/cockpit-project/starter-kit'
|
||||
license=(LGPL)
|
||||
source=("SOURCE")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
package() {
|
||||
depends=(cockpit)
|
||||
cd $pkgname
|
||||
make DESTDIR="$pkgdir" install PREFIX=/usr
|
||||
}
|
||||
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.
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
Name: cockpit-starter-kit
|
||||
Version: %{VERSION}
|
||||
Release: 1%{?dist}
|
||||
Summary: Cockpit Starter Kit Example Module
|
||||
License: LGPL-2.1-or-later
|
||||
|
||||
Source0: https://github.com/cockpit-project/starter-kit/releases/download/%{version}/%{name}-%{version}.tar.xz
|
||||
Source1: https://github.com/cockpit-project/starter-kit/releases/download/%{version}/%{name}-node-%{version}.tar.xz
|
||||
BuildArch: noarch
|
||||
%if ! 0%{?suse_version}
|
||||
ExclusiveArch: %{nodejs_arches} noarch
|
||||
%endif
|
||||
%if ! 0%{?rhel} || 0%{?rhel} >= 10
|
||||
BuildRequires: nodejs >= 18
|
||||
%endif
|
||||
BuildRequires: make
|
||||
%if 0%{?suse_version}
|
||||
# Suse's package has a different name
|
||||
BuildRequires: appstream-glib
|
||||
%else
|
||||
BuildRequires: libappstream-glib
|
||||
%endif
|
||||
BuildRequires: gettext
|
||||
%if 0%{?rhel} && 0%{?rhel} <= 8
|
||||
BuildRequires: libappstream-glib-devel
|
||||
%endif
|
||||
|
||||
Requires: cockpit-bridge
|
||||
|
||||
%{NPM_PROVIDES}
|
||||
|
||||
%description
|
||||
Cockpit Starter Kit Example Module
|
||||
|
||||
%prep
|
||||
%autosetup -n %{name} -a 1
|
||||
# ignore pre-built bundle in release tarball and rebuild it
|
||||
# but keep it in RHEL/CentOS-8/9, as that has a too old nodejs
|
||||
%if ! 0%{?rhel} || 0%{?rhel} >= 10
|
||||
rm -rf dist
|
||||
%endif
|
||||
|
||||
%build
|
||||
NODE_ENV=production make
|
||||
|
||||
%install
|
||||
%make_install PREFIX=/usr
|
||||
|
||||
# drop source maps, they are large and just for debugging
|
||||
find %{buildroot}%{_datadir}/cockpit/ -name '*.map' | xargs --no-run-if-empty rm --verbose
|
||||
|
||||
%check
|
||||
appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/*
|
||||
|
||||
# this can't be meaningfully tested during package build; tests happen through
|
||||
# FMF (see plans/all.fmf) during package gating
|
||||
|
||||
%files
|
||||
%doc README.md
|
||||
%license LICENSE dist/index.js.LEGAL.txt
|
||||
%{_datadir}/cockpit/*
|
||||
%{_datadir}/metainfo/*
|
||||
|
||||
%changelog
|
||||
64
packit.yaml
64
packit.yaml
|
|
@ -1,8 +1,14 @@
|
|||
# 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-starter-kit.spec
|
||||
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
|
||||
|
||||
|
|
@ -12,27 +18,29 @@ srpm_build_deps:
|
|||
|
||||
actions:
|
||||
post-upstream-clone:
|
||||
- make cockpit-starter-kit.spec
|
||||
# replace Source1 manually, as create-archive: can't handle multiple tarballs
|
||||
- make node-cache
|
||||
- sh -c 'sed -i "/^Source1:/ s/https:.*/$(ls *-node*.tar.xz)/" cockpit-*.spec'
|
||||
- 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: tests
|
||||
trigger: pull_request
|
||||
targets: &test_targets
|
||||
- fedora-all
|
||||
- fedora-latest-aarch64
|
||||
- centos-stream-9
|
||||
- centos-stream-9-aarch64
|
||||
- centos-stream-10
|
||||
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
targets: *test_targets
|
||||
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
|
||||
|
|
@ -45,18 +53,18 @@ jobs:
|
|||
# - 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: propose_downstream
|
||||
trigger: release
|
||||
dist_git_branches:
|
||||
- fedora-all
|
||||
|
||||
#- job: koji_build
|
||||
# trigger: commit
|
||||
# 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
|
||||
- job: bodhi_update
|
||||
trigger: commit
|
||||
dist_git_branches:
|
||||
# rawhide updates are created automatically
|
||||
- fedora-branched
|
||||
|
|
|
|||
16
po/de.po
16
po/de.po
|
|
@ -14,26 +14,10 @@ msgstr ""
|
|||
"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:43
|
||||
msgid "Running on $0"
|
||||
msgstr "Läuft auf $0"
|
||||
|
||||
#: org.cockpit-project.starter-kit.metainfo.xml:6
|
||||
msgid "Scaffolding for a cockpit module"
|
||||
msgstr "Gerüst für ein Cockpit-Modul"
|
||||
|
||||
#: org.cockpit-project.starter-kit.metainfo.xml:8
|
||||
msgid "Scaffolding for a cockpit module."
|
||||
msgstr "Gerüst für ein Cockpit-Modul."
|
||||
|
||||
#: src/manifest.json:0 org.cockpit-project.starter-kit.metainfo.xml:5
|
||||
msgid "Starter Kit"
|
||||
msgstr "Bausatz"
|
||||
|
||||
#: src/app.jsx:29
|
||||
msgid "Unknown"
|
||||
msgstr "Unbekannt"
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
[tool.mypy]
|
||||
follow_imports = 'silent' # https://github.com/python-lsp/pylsp-mypy/issues/81
|
||||
scripts_are_modules = true # allow checking all scripts in one invocation
|
||||
explicit_package_bases = true
|
||||
mypy_path = 'test/common:test:bots'
|
||||
exclude = [
|
||||
"bots"
|
||||
]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
ignore_missing_imports = true
|
||||
module = [
|
||||
# run without bots checked out
|
||||
"machine.*",
|
||||
"testvm",
|
||||
|
||||
# run without gobject-introspection
|
||||
"gi.*",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".git/",
|
||||
"modules/",
|
||||
"node_modules/",
|
||||
]
|
||||
line-length = 118
|
||||
src = []
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"A", # flake8-builtins
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"D300", # pydocstyle: Forbid ''' in docstrings
|
||||
"E", # pycodestyle
|
||||
"EXE", # flake8-executable
|
||||
"F", # pyflakes
|
||||
"FBT", # flake8-boolean-trap
|
||||
"G", # flake8-logging-format
|
||||
"I", # isort
|
||||
"ICN", # flake8-import-conventions
|
||||
"ISC", # flake8-implicit-str-concat
|
||||
"PLE", # pylint errors
|
||||
"PGH", # pygrep-hooks
|
||||
"RSE", # flake8-raise
|
||||
"RUF", # ruff rules
|
||||
"T10", # flake8-debugger
|
||||
"TCH", # flake8-type-checking
|
||||
"UP032", # f-string
|
||||
"W", # warnings (mostly whitespace)
|
||||
"YTT", # flake8-2020
|
||||
]
|
||||
ignore = [
|
||||
"FBT002", # Boolean default value in function definition
|
||||
"FBT003", # Boolean positional value in function call
|
||||
]
|
||||
|
||||
[tool.ruff.lint.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["cockpit"]
|
||||
|
||||
[tool.vulture]
|
||||
ignore_names = [
|
||||
"test[A-Z0-9]*",
|
||||
]
|
||||
41
src/app.jsx
Normal file
41
src/app.jsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 cockpit from 'cockpit';
|
||||
import React from 'react';
|
||||
import View from "./recordings.jsx";
|
||||
|
||||
const _ = cockpit.gettext;
|
||||
|
||||
export class Application extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { hostname: _("Unknown") };
|
||||
|
||||
cockpit.file('/etc/hostname').watch(content => {
|
||||
this.setState({ hostname: content.trim() });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View />
|
||||
);
|
||||
}
|
||||
}
|
||||
13
src/app.scss
13
src/app.scss
|
|
@ -3,3 +3,16 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
src/app.tsx
48
src/app.tsx
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2017 Red Hat, Inc.
|
||||
*
|
||||
* Cockpit is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Cockpit is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
|
||||
import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
|
||||
|
||||
import cockpit from 'cockpit';
|
||||
|
||||
const _ = cockpit.gettext;
|
||||
|
||||
export const Application = () => {
|
||||
const [hostname, setHostname] = useState(_("Unknown"));
|
||||
|
||||
useEffect(() => {
|
||||
const hostname = cockpit.file('/etc/hostname');
|
||||
hostname.watch(content => setHostname(content?.trim() ?? ""));
|
||||
return hostname.close;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle>Starter Kit</CardTitle>
|
||||
<CardBody>
|
||||
<Alert
|
||||
variant="info"
|
||||
title={ cockpit.format(_("Running on $0"), hostname) }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
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,7 +17,7 @@ along with this package; If not, see <http://www.gnu.org/licenses/>.
|
|||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title translate>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">
|
||||
|
|
|
|||
|
|
@ -17,16 +17,14 @@
|
|||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "cockpit-dark-theme";
|
||||
import "patternfly/patternfly-5-cockpit.scss";
|
||||
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import "cockpit-dark-theme";
|
||||
|
||||
import { Application } from './app.jsx';
|
||||
|
||||
import "patternfly/patternfly-6-cockpit.scss";
|
||||
import './app.scss';
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
createRoot(document.getElementById("app")!).render(<Application />);
|
||||
createRoot(document.getElementById("app")).render(<Application />);
|
||||
});
|
||||
|
|
@ -1,11 +1,26 @@
|
|||
{
|
||||
"version": "163.x",
|
||||
"name": "session-recording",
|
||||
|
||||
"requires": {
|
||||
"cockpit": "137"
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,26 @@
|
|||
#!/bin/sh
|
||||
set -eux
|
||||
|
||||
cd "${0%/*}/../.."
|
||||
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
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
|
@ -22,25 +34,33 @@ 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
|
||||
|
||||
sh test/vm.install
|
||||
## CSR specific setup ##
|
||||
# install cockpit-packagekit and glibc-langpack-en for testAppMenu
|
||||
dnf install -y cockpit-packagekit glibc-langpack-en
|
||||
|
||||
# Run tests in the cockpit tasks container, as unprivileged user
|
||||
CONTAINER="$(cat .cockpit-ci/container)"
|
||||
if grep -q platform:el10 /etc/os-release; then
|
||||
# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2273078
|
||||
export NETAVARK_FW=nftables
|
||||
fi
|
||||
exec podman \
|
||||
run \
|
||||
--rm \
|
||||
--shm-size=1024m \
|
||||
--security-opt=label=disable \
|
||||
--env='TEST_*' \
|
||||
--volume="${TMT_TEST_DATA}":/logs:rw,U --env=LOGS=/logs \
|
||||
--volume="$(pwd)":/source:rw,U --env=SOURCE=/source \
|
||||
--volume=/usr/lib/os-release:/run/host/usr/lib/os-release:ro \
|
||||
"${CONTAINER}" \
|
||||
sh /source/test/browser/run-test.sh
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
summary:
|
||||
Run browser integration tests on the host
|
||||
require:
|
||||
- cockpit-starter-kit
|
||||
- podman
|
||||
- cockpit-session-recording
|
||||
- tlog
|
||||
- cockpit-ws
|
||||
- cockpit-system
|
||||
- glibc-langpack-de
|
||||
- cockpit-packagekit
|
||||
- bzip2
|
||||
- git-core
|
||||
- libvirt-python3
|
||||
- make
|
||||
- nodejs
|
||||
- python3
|
||||
test: ./browser.sh
|
||||
duration: 60m
|
||||
duration: 30m
|
||||
|
|
|
|||
32
test/browser/run-test.sh
Normal file → Executable file
32
test/browser/run-test.sh
Normal file → Executable file
|
|
@ -1,40 +1,42 @@
|
|||
#!/bin/sh
|
||||
set -eux
|
||||
|
||||
cd "${SOURCE}"
|
||||
|
||||
# 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
|
||||
|
||||
. /run/host/usr/lib/os-release
|
||||
. /etc/os-release
|
||||
export TEST_OS="${ID}-${VERSION_ID/./-}"
|
||||
|
||||
if [ "$TEST_OS" = "centos-9" ]; then
|
||||
if [ "${TEST_OS#centos-}" != "$TEST_OS" ]; then
|
||||
TEST_OS="${TEST_OS}-stream"
|
||||
fi
|
||||
|
||||
# Chromium sometimes gets OOM killed on testing farm
|
||||
export TEST_BROWSER=firefox
|
||||
|
||||
EXCLUDES=""
|
||||
|
||||
# make it easy to check in logs
|
||||
echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}"
|
||||
echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}"
|
||||
|
||||
GATEWAY="$(python3 -c 'import socket; print(socket.gethostbyname("_gateway"))')"
|
||||
RC=0
|
||||
./test/common/run-tests \
|
||||
--nondestructive \
|
||||
--machine "${GATEWAY}":22 \
|
||||
--browser "${GATEWAY}":9090 \
|
||||
$EXCLUDES \
|
||||
|| RC=$?
|
||||
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
|
||||
exit $RC
|
||||
# deliver test result via exitcode file
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -1,53 +1,412 @@
|
|||
#!/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/main/test/common/testlib.py
|
||||
# See https://github.com/cockpit-project/cockpit/blob/master/test/common/testlib.py
|
||||
# "class Browser" and "class MachineCase" for the available API.
|
||||
|
||||
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.
|
||||
# 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_text(".pf-v6-c-card__title", "Starter Kit")
|
||||
def _sel_rec(self, recording):
|
||||
'''
|
||||
rec1:
|
||||
whoami
|
||||
id
|
||||
echo thisisatest123
|
||||
sleep 16
|
||||
echo thisisanothertest456
|
||||
exit
|
||||
|
||||
# verify expected host name
|
||||
hostname = m.execute("cat /etc/hostname").strip()
|
||||
b.wait_in_text(".pf-v6-c-alert__title", "Running on " + hostname)
|
||||
rec2:
|
||||
echo "Extra Commands"
|
||||
sudo systemctl daemon-reload
|
||||
sudo ssh root@localhost
|
||||
exit
|
||||
|
||||
# change current hostname
|
||||
self.write_file("/etc/hostname", "new-" + hostname)
|
||||
# verify new hostname name
|
||||
b.wait_in_text(".pf-v6-c-alert__title", "Running on new-" + hostname)
|
||||
binaryrec:
|
||||
mc
|
||||
exit
|
||||
'''
|
||||
recordings = {'rec1': '0f25700a28c44b599869745e5fda8b0c-7106-121e79',
|
||||
'rec2': '0f25700a28c44b599869745e5fda8b0c-7623-135541',
|
||||
'binaryrec': '976e4ef1d66741848ed35f7600b94c5c-1a0f-c1ae'}
|
||||
|
||||
# change language to German
|
||||
b.switch_to_top()
|
||||
# the menu and dialog changed several times
|
||||
b.click("#toggle-menu")
|
||||
b.click("button.display-language-menu")
|
||||
b.wait_popup('display-language-modal')
|
||||
b.click("#display-language-modal [data-value='de-de'] button")
|
||||
b.click("#display-language-modal button.pf-m-primary")
|
||||
b.wait_visible("#content")
|
||||
# menu label (from manifest) should be translated
|
||||
b.wait_text("#host-apps a[href='/starter-kit']", "Bausatz")
|
||||
# window title should be translated; this is not considered as "visible"
|
||||
self.assertIn("Bausatz", b.call_js_func("ph_text", "head title"))
|
||||
page = recordings[recording]
|
||||
|
||||
b.go("/starter-kit")
|
||||
b.enter_page("/starter-kit")
|
||||
# page label (from js) should be translated
|
||||
b.wait_in_text(".pf-v6-c-alert__title", "Läuft auf")
|
||||
self.browser.go(f"/session-recording#/{page}")
|
||||
|
||||
def _term_line(self, lineno):
|
||||
return f".xterm-accessibility-tree div:nth-child({lineno})"
|
||||
|
||||
if __name__ == '__main__':
|
||||
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 +1 @@
|
|||
fedora-35
|
||||
fedora-36
|
||||
|
|
|
|||
5
test/run
5
test/run
|
|
@ -8,6 +8,7 @@ TEST_SCENARIO="${TEST_SCENARIO:-}"
|
|||
[ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox
|
||||
export RUN_TESTS_OPTIONS=--track-naughties
|
||||
|
||||
make codecheck
|
||||
# linters are off by default for production builds, but we want to run them in CI
|
||||
export LINT=1
|
||||
|
||||
make check
|
||||
make po/starter-kit.pot
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
set -eux
|
||||
|
||||
# don't force https:// (self-signed cert)
|
||||
mkdir -p /etc/cockpit
|
||||
printf "[WebService]\\nAllowUnencrypted=true\\n" > /etc/cockpit/cockpit.conf
|
||||
|
||||
if systemctl is-active -q firewalld.service; then
|
||||
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,22 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2020"
|
||||
],
|
||||
"paths": {
|
||||
"*": ["./pkg/lib/*"]
|
||||
},
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "es2020"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue