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",
|
"ecmaVersion": "2022",
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": ["react", "react-hooks"],
|
"plugins": ["flowtype", "react", "react-hooks"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": ["error", 4,
|
"indent": ["error", 4,
|
||||||
{
|
{
|
||||||
|
|
@ -37,25 +37,12 @@
|
||||||
"quotes": "off",
|
"quotes": "off",
|
||||||
"react/jsx-curly-spacing": "off",
|
"react/jsx-curly-spacing": "off",
|
||||||
"react/jsx-indent-props": "off",
|
"react/jsx-indent-props": "off",
|
||||||
"react/jsx-no-useless-fragment": "error",
|
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"space-before-function-paren": "off",
|
"space-before-function-paren": "off",
|
||||||
"standard/no-callback-literal": "off"
|
"standard/no-callback-literal": "off"
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"require": "readonly",
|
"require": false,
|
||||||
"module": "readonly"
|
"module": false
|
||||||
},
|
}
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["**/*.ts", "**/*.tsx"],
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": ["./tsconfig.json"]
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
# Create a GitHub upstream release
|
||||||
# name and enable this by dropping the ".disabled" suffix from the file name.
|
|
||||||
# See README.md.
|
# See README.md.
|
||||||
name: release
|
name: release
|
||||||
on:
|
on:
|
||||||
|
|
@ -11,20 +10,20 @@ jobs:
|
||||||
source:
|
source:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/cockpit-project/tasks:latest
|
image: ghcr.io/cockpit-project/unit-tests
|
||||||
options: --user root
|
options: --user root
|
||||||
permissions:
|
permissions:
|
||||||
# create GitHub release
|
# create GitHub release
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
|
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
|
||||||
- name: Pacify git's permission check
|
- 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
|
- name: Workaround for https://github.com/actions/checkout/pull/697
|
||||||
run: git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags)
|
run: git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags)
|
||||||
|
|
@ -33,6 +32,6 @@ jobs:
|
||||||
run: make dist
|
run: make dist
|
||||||
|
|
||||||
- name: Publish GitHub release
|
- name: Publish GitHub release
|
||||||
uses: cockpit-project/action-release@7d2e2657382e8d34f88a24b5987f2b81ea165785
|
uses: cockpit-project/action-release@88d994da62d1451c7073e26748c18413fcdf46e9
|
||||||
with:
|
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:
|
*.retry
|
||||||
# - general patterns (match in all directories)
|
*.tar.xz
|
||||||
# - patterns to match files at the toplevel
|
|
||||||
# - patterns to match files in subdirs
|
|
||||||
|
|
||||||
# general patterns
|
|
||||||
*.pyc
|
|
||||||
*.rpm
|
*.rpm
|
||||||
|
node_modules/
|
||||||
# toplevel (/...)
|
dist/
|
||||||
/Test*.html
|
/*.spec
|
||||||
/Test*.json
|
/.vagrant
|
||||||
/Test*.log
|
package-lock.json
|
||||||
/Test*.log.gz
|
Test*FAIL*
|
||||||
/Test*.png
|
|
||||||
/*.whl
|
|
||||||
/bots
|
/bots
|
||||||
/cockpit-*.tar.xz
|
test/common/
|
||||||
/cockpit-navigator.spec
|
test/images/
|
||||||
/dist/
|
pkg
|
||||||
/package-lock.json
|
*.pot
|
||||||
/pkg/
|
POTFILES*
|
||||||
/node_modules/
|
tmp/
|
||||||
/tmp/
|
/po/LINGUAS
|
||||||
/tools/
|
/tools
|
||||||
|
|
||||||
# subdirs (/subdir/...)
|
|
||||||
/packaging/arch/PKGBUILD
|
|
||||||
/packaging/debian/changelog
|
|
||||||
/po/*.pot
|
|
||||||
/po/LINGUAS
|
|
||||||
/test/common/
|
|
||||||
/test/images/
|
|
||||||
/test/static-code
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"extends": "stylelint-config-standard-scss",
|
"extends": "stylelint-config-standard-scss",
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"declaration-colon-newline-after": null,
|
||||||
|
"selector-list-comma-newline-after": null,
|
||||||
|
|
||||||
"at-rule-empty-line-before": null,
|
"at-rule-empty-line-before": null,
|
||||||
|
"declaration-colon-space-before": null,
|
||||||
"declaration-empty-line-before": null,
|
"declaration-empty-line-before": null,
|
||||||
"custom-property-empty-line-before": null,
|
"custom-property-empty-line-before": null,
|
||||||
"comment-empty-line-before": null,
|
"comment-empty-line-before": null,
|
||||||
|
|
@ -15,13 +19,13 @@
|
||||||
"declaration-block-single-line-max-declarations": null,
|
"declaration-block-single-line-max-declarations": null,
|
||||||
"font-family-no-duplicate-names": null,
|
"font-family-no-duplicate-names": null,
|
||||||
"function-url-quotes": null,
|
"function-url-quotes": null,
|
||||||
|
"indentation": null,
|
||||||
"keyframes-name-pattern": null,
|
"keyframes-name-pattern": null,
|
||||||
|
"max-line-length": null,
|
||||||
"no-descending-specificity": null,
|
"no-descending-specificity": null,
|
||||||
"no-duplicate-selectors": null,
|
"no-duplicate-selectors": null,
|
||||||
"scss/at-extend-no-missing-placeholder": null,
|
"scss/at-extend-no-missing-placeholder": null,
|
||||||
"scss/load-partial-extension": null,
|
"scss/at-import-partial-extension": null,
|
||||||
"scss/at-import-no-partial-leading-underscore": null,
|
|
||||||
"scss/load-no-partial-leading-underscore": true,
|
|
||||||
"scss/at-mixin-pattern": null,
|
"scss/at-mixin-pattern": null,
|
||||||
"scss/comment-no-empty": null,
|
"scss/comment-no-empty": null,
|
||||||
"scss/dollar-variable-pattern": 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)
|
RPM_NAME := cockpit-$(PACKAGE_NAME)
|
||||||
VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.')
|
VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.')
|
||||||
ifeq ($(TEST_OS),)
|
ifeq ($(TEST_OS),)
|
||||||
TEST_OS = centos-9-stream
|
TEST_OS = centos-8-stream
|
||||||
endif
|
endif
|
||||||
export TEST_OS
|
export TEST_OS
|
||||||
TARFILE=$(RPM_NAME)-$(VERSION).tar.xz
|
TARFILE=$(RPM_NAME)-$(VERSION).tar.xz
|
||||||
NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz
|
NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz
|
||||||
SPEC=$(RPM_NAME).spec
|
SPEC=$(RPM_NAME).spec
|
||||||
PREFIX ?= /usr/local
|
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)
|
VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS)
|
||||||
# stamp file to check for node_modules/
|
# stamp file to check for node_modules/
|
||||||
NODE_MODULES_TEST=package-lock.json
|
NODE_MODULES_TEST=package-lock.json
|
||||||
|
|
@ -32,7 +32,7 @@ COCKPIT_REPO_FILES = \
|
||||||
$(NULL)
|
$(NULL)
|
||||||
|
|
||||||
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
|
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_FILES): $(COCKPIT_REPO_STAMP)
|
||||||
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
|
||||||
|
|
@ -48,21 +48,19 @@ $(COCKPIT_REPO_STAMP): Makefile
|
||||||
LINGUAS=$(basename $(notdir $(wildcard po/*.po)))
|
LINGUAS=$(basename $(notdir $(wildcard po/*.po)))
|
||||||
|
|
||||||
po/$(PACKAGE_NAME).js.pot:
|
po/$(PACKAGE_NAME).js.pot:
|
||||||
xgettext --default-domain=$(PACKAGE_NAME) --output=- --language=C --keyword= \
|
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ --language=C --keyword= \
|
||||||
--add-comments=Translators: \
|
|
||||||
--keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
|
--keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
|
||||||
--keyword=N_ --keyword=NC_:1c,2 \
|
--keyword=N_ --keyword=NC_:1c,2 \
|
||||||
--keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
|
--keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
|
||||||
--keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
|
--keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
|
||||||
--keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
|
--keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
|
||||||
--from-code=UTF-8 $$(find src/ -name '*.[jt]s' -o -name '*.[jt]sx') | \
|
--from-code=UTF-8 $$(find src/ -name '*.js' -o -name '*.jsx')
|
||||||
sed '/^#/ s/, c-format//' > $@
|
|
||||||
|
|
||||||
po/$(PACKAGE_NAME).html.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
|
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)
|
po/$(PACKAGE_NAME).manifest.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
|
||||||
pkg/lib/manifest2po -o $@ src/manifest.json
|
pkg/lib/manifest2po.js src/manifest.json -o $@
|
||||||
|
|
||||||
po/$(PACKAGE_NAME).metainfo.pot: $(APPSTREAMFILE)
|
po/$(PACKAGE_NAME).metainfo.pot: $(APPSTREAMFILE)
|
||||||
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ $<
|
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)) = /'); \
|
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' $< > $@
|
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
|
$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js
|
||||||
NODE_ENV=$(NODE_ENV) ./build.js
|
NODE_ENV=$(NODE_ENV) ./build.js
|
||||||
|
|
||||||
|
|
@ -92,7 +87,7 @@ watch: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf dist/
|
rm -rf dist/
|
||||||
rm -f $(SPEC) packaging/arch/PKGBUILD
|
rm -f $(SPEC)
|
||||||
rm -f po/LINGUAS
|
rm -f po/LINGUAS
|
||||||
|
|
||||||
install: $(DIST_TEST) 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
|
# pre-built dist/ (so it's not necessary) and ship package-lock.json (so that
|
||||||
# node_modules/ can be reconstructed if necessary)
|
# node_modules/ can be reconstructed if necessary)
|
||||||
$(TARFILE): export NODE_ENV=production
|
$(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
|
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)/,' \
|
tar --xz $(TAR_ARGS) -cf $(TARFILE) --transform 's,^,$(RPM_NAME)/,' \
|
||||||
--exclude packaging/$(SPEC).in --exclude node_modules \
|
--exclude packaging/$(SPEC).in --exclude node_modules \
|
||||||
$$(git ls-files) $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) \
|
$$(git ls-files) $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) $(SPEC) dist/
|
||||||
$(SPEC) packaging/arch/PKGBUILD dist/
|
|
||||||
|
|
||||||
$(NODE_CACHE): $(NODE_MODULES_TEST)
|
$(NODE_CACHE): $(NODE_MODULES_TEST)
|
||||||
tar --xz $(TAR_ARGS) -cf $@ node_modules
|
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
|
# build a VM with locally built distro pkgs installed
|
||||||
# disable networking, VM images have mock/pbuilder with the common build dependencies pre-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
|
$(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 $(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)
|
--script $(CURDIR)/test/vm.install $(TEST_OS)
|
||||||
|
|
||||||
# convenience target for the above
|
# convenience target for the above
|
||||||
|
|
@ -184,9 +179,6 @@ prepare-check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common
|
||||||
check: prepare-check
|
check: prepare-check
|
||||||
test/common/run-tests ${RUN_TESTS_OPTIONS}
|
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
|
# checkout Cockpit's bots for standard test VM images and API to launch them
|
||||||
bots: $(COCKPIT_REPO_STAMP)
|
bots: $(COCKPIT_REPO_STAMP)
|
||||||
test/common/make-bots
|
test/common/make-bots
|
||||||
|
|
|
||||||
139
README.md
139
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)
|
||||||
sudo apt install gettext nodejs npm make
|
* [FOSDEM talk](https://youtu.be/sHO5y28EHXg)
|
||||||
|
|
||||||
On Fedora:
|
GitHub Organization:
|
||||||
|
|
||||||
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
|
# 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:
|
These commands check out the source and build it into the `dist/` directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/cockpit-project/starter-kit.git
|
git clone https://github.com/Scribery/cockpit-session-recording.git
|
||||||
cd starter-kit
|
cd cockpit-session-recording
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -39,7 +47,7 @@ this manually:
|
||||||
|
|
||||||
```
|
```
|
||||||
mkdir -p ~/.local/share/cockpit
|
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
|
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
|
[watch mode](https://esbuild.github.io/api/#watch) to
|
||||||
automatically update the bundle on every code change with
|
automatically update the bundle on every code change with
|
||||||
|
|
||||||
./build.js -w
|
$ ./build.js -w
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
make watch
|
$ make watch
|
||||||
|
|
||||||
When developing against a virtual machine, watch mode can also automatically upload
|
When developing against a virtual machine, watch mode can also automatically upload
|
||||||
the code changes by setting the `RSYNC` environment variable to
|
the code changes by setting the `RSYNC` environment variable to
|
||||||
the remote hostname.
|
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
|
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
|
set to upload code changes to `~/.local/share/cockpit/` instead of
|
||||||
`/usr/local`.
|
`/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
|
To "uninstall" the locally installed version, run `make devel-uninstall`, or
|
||||||
remove manually the symlink:
|
remove manually the symlink:
|
||||||
|
|
@ -75,17 +83,17 @@ remove manually the symlink:
|
||||||
# Running eslint
|
# Running eslint
|
||||||
|
|
||||||
Cockpit Starter Kit uses [ESLint](https://eslint.org/) to automatically check
|
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:
|
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:
|
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.
|
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
|
Cockpit uses [Stylelint](https://stylelint.io/) to automatically check CSS code
|
||||||
style in `.css` and `scss` files.
|
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:
|
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:
|
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.
|
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
|
# Running tests locally
|
||||||
|
|
||||||
Run `make check` to build an RPM, install it into a standard Cockpit test VM
|
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
|
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
|
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
|
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
|
the VM, possibly with extra options for tracing and halting on test failures
|
||||||
(for interactive debugging):
|
(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:
|
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:
|
You can also run the test against a different Cockpit image, for example:
|
||||||
|
|
||||||
TEST_OS=fedora-40 make check
|
TEST_OS=fedora-34 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)
|
|
||||||
|
|
|
||||||
20
build.js
20
build.js
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
|
||||||
import copy from 'esbuild-plugin-copy';
|
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 { cockpitPoEsbuildPlugin } from './pkg/lib/cockpit-po-plugin.js';
|
||||||
import { cockpitRsyncEsbuildPlugin } from './pkg/lib/cockpit-rsync-plugin.js';
|
import { cockpitRsyncEsbuildPlugin } from './pkg/lib/cockpit-rsync-plugin.js';
|
||||||
import { esbuildStylesPlugins } from './pkg/lib/esbuild-common.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 production = process.env.NODE_ENV === 'production';
|
||||||
const useWasm = os.arch() !== 'x64';
|
const useWasm = os.arch() !== 'x64';
|
||||||
const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default;
|
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();
|
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('-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('-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();
|
const args = parser.parse_args();
|
||||||
|
|
||||||
if (args.rsync)
|
if (args.rsync)
|
||||||
|
|
@ -52,12 +55,15 @@ function notifyEndPlugin() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cwd = process.cwd();
|
||||||
|
|
||||||
// similar to fs.watch(), but recursively watches all subdirectories
|
// similar to fs.watch(), but recursively watches all subdirectories
|
||||||
function watch_dirs(dir, on_change) {
|
function watch_dirs(dir, on_change) {
|
||||||
const callback = (ev, dir, fname) => {
|
const callback = (ev, dir, fname) => {
|
||||||
// only listen for "change" events, as renames are noisy
|
// only listen for "change" events, as renames are noisy
|
||||||
// ignore hidden files
|
// ignore hidden files
|
||||||
if (ev !== "change" || fname.startsWith('.')) {
|
const isHidden = /^\./.test(fname);
|
||||||
|
if (ev !== "change" || isHidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
on_change(path.join(dir, fname));
|
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
|
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
|
legalComments: 'external', // Move all legal comments to a .LEGAL.txt file
|
||||||
loader: { ".js": "jsx" },
|
loader: { ".js": "jsx" },
|
||||||
metafile: !!args.metafile,
|
|
||||||
minify: production,
|
minify: production,
|
||||||
nodePaths,
|
nodePaths,
|
||||||
outdir,
|
outdir,
|
||||||
target: ['es2020'],
|
target: ['es2020'],
|
||||||
plugins: [
|
plugins: [
|
||||||
cleanPlugin(),
|
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
|
// 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
|
// in the code. This is a problem for index.html and manifest.json which are not imported
|
||||||
copy({
|
copy({
|
||||||
|
|
@ -107,10 +114,7 @@ const context = await esbuild.context({
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await context.rebuild();
|
await context.rebuild();
|
||||||
if (args.metafile) {
|
|
||||||
fs.writeFileSync(args.metafile, JSON.stringify(result.metafile));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!args.watch)
|
if (!args.watch)
|
||||||
process.exit(1);
|
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",
|
"name": "session-recording",
|
||||||
"description": "Scaffolding for a cockpit module",
|
"description": "Module for Cockpit which provides session recording configuration and playback",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "git@github.com:cockpit/starter-kit.git",
|
"repository": "git@github.com:Scribery/cockpit-session-recording.git",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "LGPL-2.1",
|
"license": "LGPL-2.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -12,48 +12,56 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "ESBUILD_WATCH='true' ./build.js",
|
"watch": "ESBUILD_WATCH='true' ./build.js",
|
||||||
"build": "./build.js",
|
"build": "./build.js",
|
||||||
"eslint": "eslint src/",
|
"eslint": "eslint --ext .js --ext .jsx src/",
|
||||||
"eslint:fix": "eslint --fix src/",
|
"eslint:fix": "eslint --fix --ext .js --ext .jsx src/",
|
||||||
"stylelint": "stylelint src/*{.css,scss}",
|
"stylelint": "stylelint src/*{.css,scss}",
|
||||||
"stylelint:fix": "stylelint --fix src/*{.css,scss}"
|
"stylelint:fix": "stylelint --fix src/*{.css,scss}"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "18.3.13",
|
"argparse": "^2.0.1",
|
||||||
"@types/react-dom": "18.3.1",
|
"chrome-remote-interface": "^0.32.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.38.0",
|
"esbuild": "0.18.6",
|
||||||
"argparse": "2.0.1",
|
"esbuild-plugin-copy": "^2.1.1",
|
||||||
"esbuild": "0.25.8",
|
"esbuild-plugin-replace": "^1.3.0",
|
||||||
"esbuild-plugin-copy": "2.1.1",
|
"esbuild-sass-plugin": "2.10.0",
|
||||||
"esbuild-plugin-replace": "1.4.0",
|
"esbuild-wasm": "^0.18.6",
|
||||||
"esbuild-sass-plugin": "3.3.1",
|
"eslint": "^8.13.0",
|
||||||
"esbuild-wasm": "0.25.8",
|
"eslint-config-standard": "^17.0.0-1",
|
||||||
"eslint": "8.57.1",
|
"eslint-config-standard-jsx": "^11.0.0-1",
|
||||||
"eslint-config-standard": "17.1.0",
|
"eslint-config-standard-react": "^13.0.0",
|
||||||
"eslint-config-standard-jsx": "11.0.0",
|
"eslint-plugin-flowtype": "^8.0.3",
|
||||||
"eslint-config-standard-react": "13.0.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-node": "11.1.0",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-plugin-promise": "6.6.0",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react-hooks": "^4.4.0",
|
||||||
"eslint-plugin-react-hooks": "4.6.2",
|
"gettext-parser": "2.0.0",
|
||||||
"gettext-parser": "8.0.0",
|
"htmlparser": "^1.7.7",
|
||||||
"glob": "11.0.3",
|
"jed": "^1.1.1",
|
||||||
"jed": "1.1.1",
|
"qunit": "^2.9.3",
|
||||||
"qunit": "2.24.1",
|
"sass": "^1.61.0",
|
||||||
"sass": "1.79.6",
|
"sizzle": "^2.3.3",
|
||||||
"stylelint": "16.22.0",
|
"stylelint": "^15.10.1",
|
||||||
"stylelint-config-recommended-scss": "15.0.1",
|
"stylelint-config-standard": "^34.0.0",
|
||||||
"stylelint-config-standard": "38.0.0",
|
"stylelint-config-standard-scss": "^10.0.0",
|
||||||
"stylelint-config-standard-scss": "15.0.1",
|
"stylelint-formatter-pretty": "^3.2.0"
|
||||||
"stylelint-formatter-pretty": "4.0.1",
|
|
||||||
"typescript": "5.8.3"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@patternfly/patternfly": "6.1.0",
|
"@patternfly/patternfly": "5.0.4",
|
||||||
"@patternfly/react-core": "6.1.0",
|
"@patternfly/react-core": "5.0.1",
|
||||||
"@patternfly/react-icons": "6.1.0",
|
"@patternfly/react-icons": "5.0.1",
|
||||||
"@patternfly/react-styles": "6.3.0",
|
"@patternfly/react-styles": "5.0.1",
|
||||||
"react": "18.3.1",
|
"@patternfly/react-table": "5.0.1",
|
||||||
"react-dom": "18.3.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/
|
# 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/
|
# 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
|
# 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
|
# use the nicely formatted release description from our upstream release, instead of git shortlog
|
||||||
copy_upstream_release_description: true
|
copy_upstream_release_description: true
|
||||||
|
|
||||||
|
|
@ -12,27 +18,29 @@ srpm_build_deps:
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
post-upstream-clone:
|
post-upstream-clone:
|
||||||
- make cockpit-starter-kit.spec
|
- make cockpit-session-recording.spec
|
||||||
# replace Source1 manually, as create-archive: can't handle multiple tarballs
|
|
||||||
- make node-cache
|
|
||||||
- sh -c 'sed -i "/^Source1:/ s/https:.*/$(ls *-node*.tar.xz)/" cockpit-*.spec'
|
|
||||||
create-archive: make dist
|
create-archive: make dist
|
||||||
# starter-kit.git has no release tags; your project can drop this once you have a release
|
# starter-kit.git has no release tags; your project can drop this once you have a release
|
||||||
get-current-version: make print-version
|
get-current-version: make print-version
|
||||||
|
|
||||||
jobs:
|
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
|
- job: copr_build
|
||||||
trigger: pull_request
|
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
|
# Build releases in COPR: https://packit.dev/docs/configuration/#copr_build
|
||||||
#- job: copr_build
|
#- job: copr_build
|
||||||
|
|
@ -45,18 +53,18 @@ jobs:
|
||||||
# - centos-stream-9-x86_64
|
# - centos-stream-9-x86_64
|
||||||
|
|
||||||
# Build releases in Fedora: https://packit.dev/docs/configuration/#propose_downstream
|
# Build releases in Fedora: https://packit.dev/docs/configuration/#propose_downstream
|
||||||
#- job: propose_downstream
|
- job: propose_downstream
|
||||||
# trigger: release
|
trigger: release
|
||||||
# dist_git_branches:
|
dist_git_branches:
|
||||||
# - fedora-all
|
- fedora-all
|
||||||
|
|
||||||
#- job: koji_build
|
- job: koji_build
|
||||||
# trigger: commit
|
trigger: commit
|
||||||
# dist_git_branches:
|
dist_git_branches:
|
||||||
# - fedora-all
|
- fedora-all
|
||||||
|
|
||||||
#- job: bodhi_update
|
- job: bodhi_update
|
||||||
# trigger: commit
|
trigger: commit
|
||||||
# dist_git_branches:
|
dist_git_branches:
|
||||||
# # rawhide updates are created automatically
|
# rawhide updates are created automatically
|
||||||
# - fedora-branched
|
- fedora-branched
|
||||||
|
|
|
||||||
16
po/de.po
16
po/de.po
|
|
@ -14,26 +14,10 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1\n"
|
"Plural-Forms: nplurals=2; plural=n != 1\n"
|
||||||
|
|
||||||
#: src/index.html:20
|
|
||||||
msgid "Cockpit Starter Kit"
|
|
||||||
msgstr "Cockpit Bausatz"
|
|
||||||
|
|
||||||
#: src/app.jsx:43
|
#: src/app.jsx:43
|
||||||
msgid "Running on $0"
|
msgid "Running on $0"
|
||||||
msgstr "Läuft auf $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
|
#: src/app.jsx:29
|
||||||
msgid "Unknown"
|
msgid "Unknown"
|
||||||
msgstr "Unbekannt"
|
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 {
|
p {
|
||||||
font-weight: bold;
|
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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title translate>Cockpit Starter Kit</title>
|
<title translate>Cockpit Session Recording</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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/>.
|
* 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 React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import "cockpit-dark-theme";
|
|
||||||
|
|
||||||
import { Application } from './app.jsx';
|
import { Application } from './app.jsx';
|
||||||
|
|
||||||
import "patternfly/patternfly-6-cockpit.scss";
|
|
||||||
import './app.scss';
|
import './app.scss';
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
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": {
|
"requires": {
|
||||||
"cockpit": "137"
|
"cockpit": "137"
|
||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"menu": {
|
||||||
"index": {
|
"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
|
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
|
# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2033020
|
||||||
dnf update -y pam || true
|
dnf update -y pam || true
|
||||||
|
|
||||||
# allow test to set up things on the machine
|
# install firefox (available everywhere in Fedora and RHEL)
|
||||||
mkdir -p /root/.ssh
|
# we don't need the H.264 codec, and it is sometimes not available (rhbz#2005760)
|
||||||
curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys
|
dnf install --disablerepo=fedora-cisco-openh264 -y --setopt=install_weak_deps=False firefox
|
||||||
chmod 600 /root/.ssh/authorized_keys
|
|
||||||
|
# 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
|
# create user account for logging in
|
||||||
if ! id admin 2>/dev/null; then
|
if ! id admin 2>/dev/null; then
|
||||||
|
|
@ -22,25 +34,33 @@ echo root:foobar | chpasswd
|
||||||
# avoid sudo lecture during tests
|
# avoid sudo lecture during tests
|
||||||
su -c 'echo foobar | sudo --stdin whoami' - admin
|
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
|
# disable core dumps, we rather investigate them upstream where test VMs are accessible
|
||||||
echo core > /proc/sys/kernel/core_pattern
|
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
|
mkdir -p /var/log/journal/
|
||||||
CONTAINER="$(cat .cockpit-ci/container)"
|
cp $FILES/1.journal /var/log/journal/1.journal
|
||||||
if grep -q platform:el10 /etc/os-release; then
|
cp $FILES/binary-rec.journal /var/log/journal/binary-rec.journal
|
||||||
# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2273078
|
|
||||||
export NETAVARK_FW=nftables
|
systemctl enable --now cockpit.socket
|
||||||
fi
|
|
||||||
exec podman \
|
# Run tests as unprivileged user
|
||||||
run \
|
# once we drop support for RHEL 8, use this:
|
||||||
--rm \
|
# runuser -u runtest --whitelist-environment=TEST_BROWSER,TEST_ALLOW_JOURNAL_MESSAGES,TEST_AUDIT_NO_SELINUX,SOURCE,LOGS $TESTS/run-test.sh
|
||||||
--shm-size=1024m \
|
runuser -u runtest --preserve-environment env USER=runtest HOME=$(getent passwd runtest | cut -f6 -d:) $TESTS/run-test.sh
|
||||||
--security-opt=label=disable \
|
|
||||||
--env='TEST_*' \
|
RC=$(cat $LOGS/exitcode)
|
||||||
--volume="${TMT_TEST_DATA}":/logs:rw,U --env=LOGS=/logs \
|
exit ${RC:-1}
|
||||||
--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
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
summary:
|
|
||||||
Run browser integration tests on the host
|
|
||||||
require:
|
require:
|
||||||
- cockpit-starter-kit
|
- cockpit-session-recording
|
||||||
- podman
|
- tlog
|
||||||
- cockpit-ws
|
- cockpit-ws
|
||||||
- cockpit-system
|
- cockpit-system
|
||||||
- glibc-langpack-de
|
- cockpit-packagekit
|
||||||
|
- bzip2
|
||||||
|
- git-core
|
||||||
|
- libvirt-python3
|
||||||
|
- make
|
||||||
|
- nodejs
|
||||||
|
- python3
|
||||||
test: ./browser.sh
|
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
|
set -eux
|
||||||
|
|
||||||
cd "${SOURCE}"
|
|
||||||
|
|
||||||
# tests need cockpit's bots/ libraries and test infrastructure
|
# tests need cockpit's bots/ libraries and test infrastructure
|
||||||
|
cd $SOURCE
|
||||||
git init
|
git init
|
||||||
rm -f bots # common local case: existing bots symlink
|
rm -f bots # common local case: existing bots symlink
|
||||||
make bots test/common
|
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
|
# disable detection of affected tests; testing takes too long as there is no parallelization
|
||||||
mv .git dot-git
|
mv .git dot-git
|
||||||
|
|
||||||
. /run/host/usr/lib/os-release
|
. /etc/os-release
|
||||||
export TEST_OS="${ID}-${VERSION_ID/./-}"
|
export TEST_OS="${ID}-${VERSION_ID/./-}"
|
||||||
|
|
||||||
if [ "$TEST_OS" = "centos-9" ]; then
|
if [ "${TEST_OS#centos-}" != "$TEST_OS" ]; then
|
||||||
TEST_OS="${TEST_OS}-stream"
|
TEST_OS="${TEST_OS}-stream"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Chromium sometimes gets OOM killed on testing farm
|
|
||||||
export TEST_BROWSER=firefox
|
|
||||||
|
|
||||||
EXCLUDES=""
|
EXCLUDES=""
|
||||||
|
|
||||||
# make it easy to check in logs
|
# make it easy to check in logs
|
||||||
echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}"
|
echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}"
|
||||||
echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}"
|
echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}"
|
||||||
|
|
||||||
GATEWAY="$(python3 -c 'import socket; print(socket.gethostbyname("_gateway"))')"
|
|
||||||
RC=0
|
RC=0
|
||||||
./test/common/run-tests \
|
test/common/run-tests --nondestructive --machine 127.0.0.1:22 --browser 127.0.0.1:9090 $EXCLUDES || RC=$?
|
||||||
--nondestructive \
|
|
||||||
--machine "${GATEWAY}":22 \
|
|
||||||
--browser "${GATEWAY}":9090 \
|
|
||||||
$EXCLUDES \
|
|
||||||
|| RC=$?
|
|
||||||
|
|
||||||
echo $RC > "$LOGS/exitcode"
|
echo $RC > "$LOGS/exitcode"
|
||||||
cp --verbose Test* "$LOGS" || true
|
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)
|
#!/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
|
# 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.
|
# "class Browser" and "class MachineCase" for the available API.
|
||||||
|
|
||||||
import testlib
|
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
|
# Nondestructive tests all run in the same running VM. This allows them to run in Packit, Fedora, and RHEL dist-git gating
|
||||||
# RHEL dist-git gating. They must not permanently change any file or configuration on the system in a
|
# They must not permanently change any file or configuration on the system in a way that influences other tests.
|
||||||
# way that influences other tests.
|
|
||||||
@testlib.nondestructive
|
@testlib.nondestructive
|
||||||
class TestApplication(testlib.MachineCase):
|
class TestApplication(testlib.MachineCase):
|
||||||
def testBasic(self):
|
def _login(self, loc="/session-recording", wait="#app"):
|
||||||
|
self.login_and_go(loc)
|
||||||
b = self.browser
|
b = self.browser
|
||||||
m = self.machine
|
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")
|
def _sel_rec(self, recording):
|
||||||
# verify expected heading
|
'''
|
||||||
b.wait_text(".pf-v6-c-card__title", "Starter Kit")
|
rec1:
|
||||||
|
whoami
|
||||||
|
id
|
||||||
|
echo thisisatest123
|
||||||
|
sleep 16
|
||||||
|
echo thisisanothertest456
|
||||||
|
exit
|
||||||
|
|
||||||
# verify expected host name
|
rec2:
|
||||||
hostname = m.execute("cat /etc/hostname").strip()
|
echo "Extra Commands"
|
||||||
b.wait_in_text(".pf-v6-c-alert__title", "Running on " + hostname)
|
sudo systemctl daemon-reload
|
||||||
|
sudo ssh root@localhost
|
||||||
|
exit
|
||||||
|
|
||||||
# change current hostname
|
binaryrec:
|
||||||
self.write_file("/etc/hostname", "new-" + hostname)
|
mc
|
||||||
# verify new hostname name
|
exit
|
||||||
b.wait_in_text(".pf-v6-c-alert__title", "Running on new-" + hostname)
|
'''
|
||||||
|
recordings = {'rec1': '0f25700a28c44b599869745e5fda8b0c-7106-121e79',
|
||||||
|
'rec2': '0f25700a28c44b599869745e5fda8b0c-7623-135541',
|
||||||
|
'binaryrec': '976e4ef1d66741848ed35f7600b94c5c-1a0f-c1ae'}
|
||||||
|
|
||||||
# change language to German
|
page = recordings[recording]
|
||||||
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"))
|
|
||||||
|
|
||||||
b.go("/starter-kit")
|
self.browser.go(f"/session-recording#/{page}")
|
||||||
b.enter_page("/starter-kit")
|
|
||||||
# page label (from js) should be translated
|
|
||||||
b.wait_in_text(".pf-v6-c-alert__title", "Läuft auf")
|
|
||||||
|
|
||||||
|
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()
|
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
|
[ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox
|
||||||
export RUN_TESTS_OPTIONS=--track-naughties
|
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 check
|
||||||
make po/starter-kit.pot
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
set -eux
|
set -eux
|
||||||
|
|
||||||
# don't force https:// (self-signed cert)
|
# don't force https:// (self-signed cert)
|
||||||
mkdir -p /etc/cockpit
|
|
||||||
printf "[WebService]\\nAllowUnencrypted=true\\n" > /etc/cockpit/cockpit.conf
|
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
|
firewall-cmd --add-service=cockpit --permanent
|
||||||
fi
|
fi
|
||||||
systemctl enable cockpit.socket
|
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