Compare commits

..

208 commits

Author SHA1 Message Date
Jelle van der Waa
9dce9b02e3 package.json: drop po2json
The cockpit-po-plugin was reworked to use gettext-parser directly
instead of po2json in 1e92eb6815eb48aab590790ecd67.
2023-09-20 14:20:57 +02:00
Cockpit Project
5cb865dc07 Makefile: Update Cockpit lib to 9c73bec7e1dc2395a00aa0c510fd7210
Closes #697

(cherry picked from commit dfff5fc364)
2023-09-14 14:21:49 -04:00
Cockpit Project
c6912fc484 package.json: Update @patternfly/patternfly, @patternfly/react-core, @patternfly/react-icons, @patternfly/react-styles
Closes #696

(cherry picked from commit 5f6b8d09f9)
2023-09-14 14:21:49 -04:00
Jelle van der Waa
4128c04f47 package.json: document the required nodejs version
To avoid issues about not being able to run make / npm install. This
sadly only produces a warning, but that hint might be good enough.

```
$ npm install
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: undefined,
npm WARN EBADENGINE   required: { node: '>= 18' },
npm WARN EBADENGINE   current: { node: 'v16.20.2', npm: '9.8.1' }
npm WARN EBADENGINE }
```

Fixes #693

(cherry picked from commit 2a10bd66ec)
2023-09-14 14:21:49 -04:00
Martin Pitt
9d5445a99e fmf: Plumb through $TEST_* variables for unexpected messages
This will allow us to control the value from test plans, in particular
for disabling at least some unexpected message checks for reverse
dependency testing. We don't want to disable unexpected messages
in general for fmf, as we are looking for exactly these in e.g.
selinux-policy reverse dependency tests.

Move from `su` to `runtest`, as with the former it's impossible to plumb
through variables with non-trivial characters, as they cannot be quoted.

Taken from c38692fa4c

(cherry picked from commit 9544f57220)
2023-08-28 15:13:02 -04:00
Justin Stephenson
d9b8fb2b13 tests: Use mc for testPlayBinary
cockpit doesn't seem to handle any/all non-UTF8 character data
2023-08-22 14:25:46 -04:00
Justin Stephenson
142d42066b Lock down esbuild and esbuild-sass-plugin versions
Workaround error:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: session-recording@undefined
npm ERR! Found: esbuild@0.18.20
npm ERR! node_modules/esbuild
npm ERR!   dev esbuild@"^0.18.6" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer esbuild@"^0.19.1" from esbuild-sass-plugin@2.12.0
npm ERR! node_modules/esbuild-sass-plugin
npm ERR!   dev esbuild-sass-plugin@"^2.10.0" from the root project
2023-08-16 14:14:38 -04:00
Cockpit Project
915521a6ad Makefile: Update Cockpit lib to 4133eb93dc20f00db996d1fefdd5fcbf
Closes #688

(cherry picked from commit 468cf21f4f)
2023-08-16 14:14:38 -04:00
Cockpit Project
d7eb158f79 package.json: Update @patternfly/patternfly, @patternfly/react-core, @patternfly/react-icons, @patternfly/react-styles
Closes #687

(cherry picked from commit 79e5693042)
2023-08-16 14:14:38 -04:00
Jelle van der Waa
b3c40d9a4d package.json: drop deprecated eslint-plugin-standard
eslint-config-standard no longer requires it since 16.0.0.

(cherry picked from commit 65e3e488f0)
2023-08-16 14:14:38 -04:00
Martin Pitt
ec2325a3db Revert "build: add support for /pybridge scenario"
We don't need this any more, the pybridge landed in all planned
distributions.

Do keep the more explicit and correct handling of `$TEST_BROWSER` in
test/run, though.

This reverts commit 03d02f398a.

(cherry picked from commit 826c1e29ce)
2023-08-16 14:14:38 -04:00
Justin Stephenson
27ec379aad Player: Stop making the Terminal object state
It's completely unnecessary and may cause unnecessary renders.
2023-07-27 14:01:40 -04:00
Tomas Matus
9026069b3c build.js: support flags
Adds flags to build.js to use rsync, disable linting and use watch mode.

(cherry picked from commit 3b14e61390)
2023-07-26 10:14:26 -04:00
Justin Stephenson
f05760e535 packaging: Update spec License: to SPDX format 2023-07-26 10:14:26 -04:00
Tomas Matus
cba733642f build.js: Fix LINT env variable check
(cherry picked from commit 3d75eb66d7)
2023-07-26 10:14:26 -04:00
Cockpit Project
8f9d7bb636 Makefile: Update Cockpit lib to 4693a536e3262d3254d848daed251ef3
Closes #676

(cherry picked from commit 36fc246711)
2023-07-26 10:14:26 -04:00
Justin Stephenson
a4a89eab52 Tests: Allow charset journal messages
Generated from tlog-rec-session
2023-07-24 13:08:29 -04:00
Justin Stephenson
9f5d0d42b7 Update to the new root creation function 2023-07-19 15:44:03 -04:00
Martin Pitt
ef6634a0c7 package.json: Bump stylelint to 15
(cherry picked from commit 6ece59c917)
2023-07-19 15:44:03 -04:00
Cockpit Project
73ac17ce03 Makefile: Update Cockpit lib to 1336ce350d88d385870ba56405136df7
Closes #673

(cherry picked from commit ee9ab10aeb)
2023-07-19 15:44:03 -04:00
Scott Poore
cba99b097a test: add check for sssd config id_provider proxy
The sssd files provider configuration was changed to use the proxy with
a files proxy library.  Adding a check to the testSessionRecordingConf
test case to confirm expected settings added to config stub when
created.

Signed-off-by: Scott Poore <spoore@redhat.com>
2023-07-19 08:28:35 -04:00
Justin Stephenson
a89d6d646c Tests: Allow invalid non-UTF8 journal messages
The following error is generated on fedora rawhide:

cockpit-ws[12427]: invalid non-UTF8 @data passed as text
to web_socket_connection_send()
2023-07-18 15:07:57 -04:00
Justin Stephenson
e9f6c15d70 Add xterm-canvas-addon dependency for rendering 2023-07-18 15:07:57 -04:00
Justin Stephenson
7623d95e11 Minor Eslint fixes 2023-07-18 15:07:57 -04:00
Justin Stephenson
bd2765f636 Tests: Update data list to PF5 for testAppMenu 2023-07-18 15:07:57 -04:00
Cockpit Project
523358b03d Makefile: Update Cockpit lib to 3d2d07cb751b141b6bd6ee9a3d423081
Closes #669

(cherry picked from commit 27ad7ce5a7)
2023-07-18 15:07:57 -04:00
Martin Pitt
3e29263d45 package.json: Bump esbuild and esbuild-sass-plugin
esbuild-sass-plugin 2.10 got released as compatible with 2.8, but it is
not compatible any more with esbuild 0.17.18. Bump both.

(cherry picked from commit d3b9064d63)
2023-07-18 15:07:57 -04:00
zaltark
e0c8d9769b Update org.cockpit-project.session-recording.metainfo.xml 2023-07-18 13:51:31 -04:00
Allison Karlitskaya
804a15b07f Makefile: bump our test/common dependency
... and make use of the new pywrap feature from our test.

Use the same eslint and stylelint plugin configuration as the cockpit
main repo.

Co-authored-by: Katerina Koukiou <kkoukiou@redhat.com>
(cherry picked from commit 2215aa3bf8)
2023-05-31 08:36:26 -04:00
Katerina Koukiou
8564be5f8f patternfly-5-overrides should be auto-imported by the page.scss file
page.scss sould be imported by all pages.

(cherry picked from commit 12a648b6e4)
2023-05-31 08:36:26 -04:00
Cockpit Project
ae3d2b77cb Makefile: Update Cockpit lib to 3ca979d542a4d6cf865f2132e0bdf1b0
Closes #656

(cherry picked from commit 4c405168a8)
2023-05-31 08:36:26 -04:00
Martin Pitt
0622a5e06a Drop obsolete pf-m-redhat-font class
(cherry picked from commit dc5a514625)
2023-05-31 08:36:26 -04:00
Justin Stephenson
4426f62000 Bump PF5 react-{table, tokens} not in starter kit 2023-05-24 10:45:14 -04:00
Cockpit Project
9fe6a8229a package.json: Update @patternfly/patternfly, @patternfly/react-core, @patternfly/react-icons, @patternfly/react-styles
Bump Cockpit commit to pick up the necessary adjustments for latest PF,
and adjust test for the new "-v5" namespace prefix.

Closes #654

(cherry picked from commit 092cefab3b)
2023-05-24 10:45:14 -04:00
Justin Stephenson
efba93ce98 Workaround git permission check gh actions bug 2023-05-17 11:29:49 -04:00
Justin Stephenson
12ab19bb67 Upgrade to PatternFly 5 Alpha 2023-05-17 11:08:46 -04:00
Cockpit Project
4b0bfc7648 Makefile: Update Cockpit lib to da5abbb4245b0455cc8b610efe01e684
Closes #649

(cherry picked from commit b3e97c711c)
2023-05-17 11:08:46 -04:00
Martin Pitt
7c82b32a59 package.json: Pin down versions of @patternfly/react-{styles,icons}
These are already installed as dependencies, and we do the same in other
Cockpit projects. The latest react-styles version became incompatible
with the react-core version, causing a build failure.

(cherry picked from commit a47c641af9)
2023-05-17 11:08:46 -04:00
Cockpit Project
81a3650eba Makefile: Update Cockpit lib to 947f1753867e3924b9617aaace936225
Update to PF5 and the new cockpit lib API.

Closes #646

(cherry picked from commit b2bdaac5b2)
2023-05-17 11:08:46 -04:00
Martin Pitt
4dbf79bcef Makefile: Fix watch dependencies
Unbreak `make watch` from a clean tree by ensuring that node_modules/
and pkg/lib exist.

(cherry picked from commit 6ab10901a4)
2023-05-17 11:08:46 -04:00
Justin Stephenson
0470867bd3
Update release.yml 2023-05-11 12:32:35 -04:00
Justin Stephenson
3232b641f6 Automate the release process 2023-05-11 11:48:09 -04:00
Justin Stephenson
6b7d8134f0 Add template sync to cockpit starter kit 2023-05-10 10:48:30 -04:00
Justin Stephenson
b2738e6548 Play after rewind in testFastforwardControls 2023-05-09 13:04:00 -04:00
Justin Stephenson
4367e8dd61 Set TZ to avoid CI failures with testFilter* tests 2023-05-09 09:38:34 -04:00
Justin Stephenson
0ae9d17487 testZoomSpeedControls incorrectly checks scale
Also relax the scale (value) assertion, as the scale value
switches between 0.X values in different environments.
2023-05-08 15:36:34 -04:00
Justin Stephenson
acf2d299ac Fix testSessionRecordingConf
This test creates the sssd config within the test itself
2023-05-08 13:37:21 -04:00
Justin Stephenson
cfd219f31f Read TMT_TEST_DATA variable for LOGS in browser.sh 2023-05-04 13:56:58 -04:00
Justin Stephenson
2336ba0e91 Remove Semaphore CI 2023-05-04 12:41:05 -04:00
Justin Stephenson
d589513534 Add initial packit configuration 2023-05-04 10:38:23 -04:00
Justin Stephenson
7414584afe Fix node-modules in run-test.sh 2023-05-03 15:42:27 -04:00
Justin Stephenson
806eeab1f2 Install sssd-proxy in test browser.sh 2023-05-03 14:29:14 -04:00
Justin Stephenson
5b5fc11b94 Add Files path for test recordings 2023-05-03 13:50:09 -04:00
Justin Stephenson
ce70a6d4ee FMF: Run tests together 2023-05-03 11:30:59 -04:00
Justin Stephenson
c3616baaa2 Add executable bit to browser shell scripts 2023-05-03 10:52:41 -04:00
Justin Stephenson
a378c5dbf3 FMF: Fix minor typo in plans 2023-04-28 11:31:16 -04:00
Justin Stephenson
f577208220 Add FMF tests and test scripts 2023-04-28 11:28:25 -04:00
Justin Stephenson
86f674bd92 Read existing sssd conf domains and services
Avoid overwriting services and domain sections of system with
an existing sssd.conf (IPA client for example). Copy the
services section used in sssd.conf, and append 'proxy' to
existing domain section.
2023-04-28 09:48:53 -04:00
Cockpit Project
07b5b13b12 Makefile: Update Cockpit lib to 269bf89276c679a03befc8a04244addd
(cherry picked from commit 4bc3de6d5d)
2023-04-27 14:47:24 -04:00
Justin Stephenson
aeb3cb6d8d Remove CentOS condition in testZoomSpeedControls 2023-04-27 14:47:24 -04:00
Justin Stephenson
e151c9ee8f Add accessible name label for Progress component
Fixes the error: One of aria-label or aria-labelledby properties should
be passed when using the progress component without a title.
2023-04-27 14:47:24 -04:00
Justin Stephenson
7c75596330 Remove 'enable_files_domain' from SSSD Config 2023-04-27 14:47:24 -04:00
Justin Stephenson
e9490a1a10 SSSD config changes 2023-04-27 14:47:24 -04:00
Justin Stephenson
cc2a205b13 testFastforwardControls update after player-restart
New xterm js accessibility tree no longer displays "Blank line"
2023-04-27 14:47:24 -04:00
Justin Stephenson
527941da25 Switch wait_present to wait_visible
wait_visible() is now deprecated.
2023-04-27 14:47:24 -04:00
Justin Stephenson
0089d35bef ESLint error and warning fixes 2023-04-27 14:47:24 -04:00
Justin Stephenson
b73f42eb38 Fix stylelint errors 2023-04-27 14:47:24 -04:00
Justin Stephenson
d41cf3bfcc Dependency updates 2023-04-27 14:47:24 -04:00
Justin Stephenson
235f110ec7 Bring up to date with cockpit starter kit
This encompasses a number of changes to the build process.
2023-04-27 14:47:24 -04:00
Justin Stephenson
a0fffde59d Config: Switch SSSD files provider to Proxy provider
SSSD Files provider is being deprecated and removed in later RHEL,
Fedora releases.
2023-04-17 12:09:43 -04:00
Justin Stephenson
38d4b00533 Update fedora license in spec file
https://fedoraproject.org/wiki/Changes/SPDX_Licenses_Phase_1
2023-03-02 14:11:35 -05:00
Justin Stephenson
d22ac8ef1a Makefile: Bump test API to 267 2023-02-06 15:03:49 -05:00
Jelle van der Waa
e2856e3160 Makefile: drop installing cockpit-ws/cockpit-packagekit
The default test virtual machines have this pre-installed.
2023-02-06 15:02:28 -05:00
Jelle van der Waa
8151aa2d49 src: load translations via po.js
/*/po.js is deprecated in the Cockpit bridge, po.js is how one normally
loads translations. Cockpit-bridge will figure out the correct
translations to return.
2023-02-06 15:02:28 -05:00
Jelle van der Waa
02c5b475c6 .semaphore: update TEST_OS to Fedora 36 2023-02-06 15:02:28 -05:00
Jelle van der Waa
4f44ec3e22 webpack.config.js: update compression plugin to succeed build 2023-02-06 15:02:28 -05:00
Jelle van der Waa
95723f2817 Makefile: Adjust to changed rpmspec -q behaviour
Fedora 37's rpmspec changed behaviour: `-q` now shows the source RPM
name instead of the binary one. `--srpm` and `--rpms` don't influence
this behaviour any more. So get along with both variants.
2023-02-06 15:02:28 -05:00
Martin Pitt
9e482dab7d Use standard "translate" marker in HTML
Cockpit's test-static-code complains about `translatable`, so let's use
the correct attribute to avoid spreading it further.
2023-02-06 15:02:28 -05:00
Justin Stephenson
92f3b7b75d Update dependencies excluding Patternfly
Update to webpack 5
2023-01-16 08:55:35 -05:00
Justin Stephenson
39a359bffb Remove unneeded tlog UID code
The tlog UID was being set explicitly to ensure journalctl matches
used during tests would find pre-recorded journals. This is no longer
needed as we removed the TLOG_UID filter from the journalctl match
string.
2022-06-09 10:36:25 -04:00
Justin Stephenson
6f5ec24e16 Drop moment.js dependency
Addresses CVE-2022-24785
2022-05-19 14:17:07 -04:00
Justin Stephenson
88a167a89a Tests: Assert pause state with a later command in rec1
On some systems, the 'whoami' command may still show in the terminal output
if the pause does not happen fast enough.
2022-04-19 15:53:33 -04:00
Justin Stephenson
e716567dfc CI: Update image to ubuntu 20.04 2022-04-19 15:14:37 -04:00
Justin Stephenson
bab09074b3 Enable SSSD files domain unconditionally
Use authselect to update nsswitch to work with files domain
2022-04-18 10:35:58 -04:00
Justin Stephenson
da32f4f344 Avoid crash in unmount if journal is null 2021-11-17 13:55:10 -05:00
Justin Stephenson
1d81c8e828 Sync Makefile closer to starter-kit 2021-11-17 13:05:47 -05:00
Katerina Koukiou
c855f12deb Use current babel/eslint integration
Fixes these `npm install` warnings:

> deprecated babel-eslint@10.1.0: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.
> deprecated eslint-loader@4.0.2: This loader has been deprecated. Please use eslint-webpack-plugin

Cherry-picked from starter-kit commit 0e608d562a
2021-11-17 13:05:47 -05:00
Katerina Koukiou
fec5fd1ae7 Use Flex layout for spacing the two config cards in the config page 2021-11-17 13:05:47 -05:00
Katerina Koukiou
3244a4c37d Use Page/PageSection/Breadcrumb patternfly components to make the app aligned with the PF design guidelines 2021-11-17 13:05:47 -05:00
Katerina Koukiou
3ebdcfa4fe Move to xz dist tarballs
For consistency with the already xz'ed node tarball.

Rename the oddly named `dist-gzip` target to the standard `dist`.

Cherry-picked from starter-kit commit 4ca75f143c
2021-11-17 13:05:47 -05:00
Katerina Koukiou
47c1eb0804 Remove unused slider.html file 2021-11-17 13:05:47 -05:00
Katerina Koukiou
8b9f7f490e package.json: explicitely depend on @patternfly/react-icons otherwise we get an import error 2021-11-17 13:05:47 -05:00
Katerina Koukiou
03a5445b35 webpack: Use relative resolve path for npm 7 compatibility
npm 7 changed how it resolves dependencies, and cockpit-machines fails
to build with lots of unresolved peer dependencies of PatternFly.

With an absolute path, `resolve.modules` will only look in that
directory; the default is a relative path "node_modules" that just
works [1]. We need to keep the `$SRCDIR` support, but convert the path
to a relative one to keep the old recursive search behaviour.

This magically fixes the label alignment in dialogs, update the pixel
test references accordingly.

[1] https://webpack.js.org/configuration/resolve/#resolvemodules

See also a117600dff
2021-11-17 13:05:47 -05:00
Katerina Koukiou
abc5922946 package.json: update patternfly packages 2021-11-17 13:05:47 -05:00
Katerina Koukiou
4be61896ea package.json: use sass instead of node-sass
node-sass is a compiled ELF module, which is problematic for
distributions that want to rebuild everything from source. The sassc CLI
program is packaged everywhere, and both use the same libsass library.

So drop node-sass and replace it with sass which is also what cockpit
and other external plugins are using.

Port alerts in PF-react-core and fix missing icons in alerts
2021-11-17 13:05:47 -05:00
Katerina Koukiou
e13c9d1bf3 Fetch pkg/lib automatically from cockpit instead of keeping a local copy
* remove mustache module as it's not used anymore.
* port alert component to react-core as the current code creates a
broken UI
2021-11-17 13:05:47 -05:00
Justin Stephenson
e49176b966 Update Docker image and TEST_OS run to Fedora 35 2021-11-16 08:54:31 -05:00
Justin Stephenson
7ac04a4917 Tests: Test SSSD exclude_users and exclude_groups 2021-09-10 12:45:12 -04:00
Justin Stephenson
c48e7f69d1 Config: Add SSSD exclude_users and exclude_groups
Add configuration functionality to allow sssd exclude_users, and
exclude_groups configuration options. These options are only
applicable when scope=all. Refer to man sssd-session-recording(5)
2021-09-10 12:45:12 -04:00
Justin Stephenson
2fe72717b2 TESTS: Fix testSessionRecordingConf cleanup issue
Run SSSD session recording scope=none test last
to leave system in working state.
2021-08-16 12:14:28 -04:00
Justin Stephenson
d3110ea7ef TESTS: Restart SSSD after restoring config 2021-08-12 14:06:51 -04:00
Justin Stephenson
7092e13518 TESTS: Allow metainfo journal error 2021-08-12 11:56:11 -04:00
Matej Marusak
d2708cf533 test: Enter session-recording page
Cockpit can have multiple pages opened at the same time. This is
handled through iframes. When switching between pages we need to tell
tests that we will be now working with different iframe.

Before this test was checking `b.wait_present("#app")` in `/apps` page
and not in `/session-recording` page.
2021-08-12 09:27:57 -04:00
Justin Stephenson
f7b2bdccda Tests: Fix RHEL9 filter test timing failures 2021-07-21 11:28:07 -04:00
Justin Stephenson
d038d2bd55 Throttle journalctl restarts
This addresses an issue with typeahead search generating a significant
load on the system, a single filter test run can make ~100 calls to
journalctl restart if not throttled.
2021-07-14 23:22:19 -04:00
Justin Stephenson
6379950582 CI: Update TEST_OS fedora version 2021-07-07 14:15:26 -04:00
Justin Stephenson
7ff310a705 Fix CentOS 8 stream test journal messages
audit: type=1400 audit(1625668755.740:6): avc:  denied  { getattr } for
pid=1589 comm="systemctl" name="/" dev="vda1" ino=128
scontext=system_u:system_r:cockpit_ws_t:s0
tcontext=system_u:object_r:fs_t:s0 tclass=filesystem permissive=0
2021-07-07 14:15:26 -04:00
Justin Stephenson
2c16ca3e2f Add selector conditional in testZoomSpeedControls
Fixes downstream failure
2021-04-28 11:29:21 -04:00
Justin Stephenson
e561f18f56 spec: Add BuildRequires: make
See https://fedoraproject.org/wiki/Changes/Remove_make_from_BuildRoot
2021-01-13 10:50:55 -05:00
Justin Stephenson
370f58c543 Update changelog 2021-01-13 10:50:55 -05:00
Justin Stephenson
255a8bdde1 Set timezone for Logs Correlation test
Fixes a local timezone issue with centos-8-stream
2021-01-13 10:01:00 -05:00
Justin Stephenson
fcfc5f40f8 Install cockpit-packagekit in local VM
Fix testAppMenu when running make check locally
2021-01-13 10:01:00 -05:00
Scott Poore
74bc71b190 Add Applications Menu test
testAppMenu navigates to the Applications menu before connecting to the
Session Recording page.

Update to semaphone job config to add cockpit-packagekit for Fedora 32
and Centos 8
2020-11-19 13:42:56 -05:00
Justin Stephenson
e504489ab0 Use journalctl --utc for Logs view to handle DST 2020-11-05 09:32:48 -05:00
Justin Stephenson
819da4d495 Remove bots sudo rm from Makefile 2020-11-04 21:34:59 -05:00
Justin Stephenson
0e8f87a000 Add binary recording test 2020-10-09 12:56:58 -04:00
Justin Stephenson
ada0bacaed test: Bump testlib to 229 2020-10-06 10:15:05 -04:00
Benjamin Graham
cf618957af Updated CI to work with patternfly update
* added multiple log artifacting
* fixed component ids and classes to match patternfly
2020-07-24 12:53:46 -04:00
Benjamin Graham
aa63c3871c Updated UI to use patternfly
* Removed unused css files
* Converted all UI elements to patternfly 4
* Implemented config page under same app
* Replaced slider with patternfly `Progress` component
2020-07-24 12:53:46 -04:00
Benjamin Graham
46ad9834b3 Updated dependencies to match latest starterkit
* removed unused dependencies
* updated dependencies to match starterkit
* updated build pipeline
2020-07-24 12:53:46 -04:00
Martin Pitt
7eada9f82a metainfo: Fix launchable and update description
`<launchable>` must coincide with the actual URL path defined by the
manifest.

Remove the period from <summary>, as the spec [1] suggests. Also remove
the redundant "Provides". This makes Cockpit's "Applications" page more
consistent.

[1] https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#sect-Metadata-GenericComponent

https://bugzilla.redhat.com/show_bug.cgi?id=1856639
2020-07-14 08:36:43 -04:00
Benjamin Graham
bc24785d98 Added artifact collection upon failure 2020-06-18 10:57:29 -04:00
Benjamin Graham
7d9391da3f Added testing for username and time filtering
Gave search box ids for ease of access, fixed date input bug, and added tests
`testFilterUsername`, `testFilterSince`, and `testFilterUntil`
2020-06-18 10:57:29 -04:00
Benjamin Graham
0a85319c5e Added search testing
Added test `testSearch`
2020-06-16 22:42:46 -04:00
Benjamin Graham
36173c8b9c Added test for zooming while playing at 16x
Added test `testZoomSpeedControls`
2020-06-16 22:42:46 -04:00
Benjamin Graham
8275cca551 Added log correlation testing
Added `testLogCorrelation`
2020-06-16 22:42:46 -04:00
Benjamin Graham
11fd640fe5 Fixed download error in testSessionRecordingConf 2020-06-09 13:03:54 -04:00
Benjamin Graham
7592ce8ab0 Adding a semaphore workflow to run CI 2020-06-09 13:03:54 -04:00
Benjamin Graham
fe02babb2f Simplified testing functions
Added helper functions to make tests smaller and less repetative
2020-06-03 12:30:28 -04:00
Benjamin Graham
95c92fd984 Added test for cockpit session display drag
Created testDisplayDrag to test the initialization and effect of
enabling display drag
2020-06-03 10:20:33 -04:00
Benjamin Graham
c4d2ec525b Added test for pause button and config file saving
Added the tests `testPlaybackPause` and `testSessionRecordingConf`
2020-06-03 10:00:11 -04:00
Benjamin Graham
de0b26f1e4 Removed unnecessary calls to wait_timeout 2020-06-03 10:00:11 -04:00
Benjamin Graham
5283e234a1 Fixed and simplified tests
Gave buttons IDs for ease of access, fixed `fit-to` testing to
better reflect purpose, and fixed occasional timing error in
`testSkipFrame` caused by call overlap
2020-06-03 10:00:11 -04:00
Benjamin Graham
019f61fda1 Bump cockpit test version from 199 to 219
Calls to `allow_authorize_journal_messages` are no longer needed
2020-06-03 10:00:11 -04:00
Benjamin Graham
78c850acf3 Fixed timezone issue by searching in client time 2020-06-01 15:15:56 -04:00
Justin Stephenson
179fb8c5e6 Use --all journalctl option
journalctl encodes fields with "non-printable" characters,
like unicode control characters, as an array.

This ensures that the tlog MESSAGE field is shown in full using
the journalctl API.
2020-05-28 11:05:46 -04:00
Justin Stephenson
4abce7ae8d Fix Downstream gating test issues 2020-05-26 14:51:37 -04:00
Justin Stephenson
395cbdc2c9 Move code out of deprecated componentWillUpdate()
> warning: Warning: componentWillUpdate has been renamed, and is not
recommended for use. See https://fb.me/react-unsafe-component-lifecycles
for details.

* Move data fetching code or side effects to componentDidUpdate.
2020-04-27 16:49:36 -04:00
Justin Stephenson
6a7f6805d9 Move code out of deprecated componentWillMount()
> warning: Warning: componentWillMount has been renamed, and is not
recommended for use. See https://fb.me/react-unsafe-component-lifecycles
for details.

* Move code with side effects to componentDidMount, and set initial
state in the constructor
2020-04-27 16:49:36 -04:00
Justin Stephenson
5a6e0beb53 Don't clobber cockpit bots directory 2020-04-21 13:57:27 -04:00
Justin Stephenson
198e49cfff Handle byte-array encoded journal data
Journalctl json output formats field values as JSON strings with the
exception:

  Fields containing non-printable or non-UTF8 bytes are encoded as arrays
  containing the raw bytes individually formatted as unsigned numbers.
2020-04-21 13:53:48 -04:00
Justin Stephenson
564c9c25f7 Fix rpmmacro to resolve correct path on CentOS7 2020-04-21 13:24:20 -04:00
Matej Marusak
ca14ae94ec manifest: Define documentation url 2020-02-03 11:38:06 -05:00
Matej Marusak
4bdbec47fb manifest: Define keywords 2020-02-03 11:38:06 -05:00
Matej Marusak
bd2e75f7ee manifest2po: Parse also docs from manifest 2020-02-03 11:38:06 -05:00
Matej Marusak
adc5913b76 manifest2po: Parse also keywords from manifest 2020-02-03 11:38:06 -05:00
Matej Marusak
7032a2e74f Remove unused 'manifest.json.in' 2020-02-03 11:38:06 -05:00
Justin Stephenson
d2a9be4564 Update parent id in metainfo file 2020-01-13 10:00:41 -05:00
Justin Stephenson
f2e5bc2903 Reset Logs View on Player Rewind 2019-11-07 14:37:57 -05:00
Justin Stephenson
046a1d4cb1 Expand configuration form table width 2019-11-07 14:37:36 -05:00
Martin Pitt
45c8d762a2 Makefile: Update bots target for moved GitHub project
Cockpit bots are in their own project now.

Make the target phony so that `make bots` updates an existing checkout.

Closes #228
2019-09-25 09:15:42 -04:00
Justin Stephenson
e02df0759c Minor spec file fixes
Use @VERSION@ substitution replaced from Makefile.

Remove cockpit-starter-kit example spec file.
2019-09-11 11:15:17 -04:00
Justin Stephenson
7c15858444 Optimize Performance of Slider component 2019-09-11 09:29:45 -04:00
Justin Stephenson
feed483646 Make Logs view optional 2019-09-11 09:29:45 -04:00
Justin Stephenson
811b80fa27 Make Logs component a child of Recording component 2019-09-11 09:29:45 -04:00
Justin Stephenson
5348d111c4 Fix journal matching
This allows for retrieving all tlog recordings and fixes
the broken username/hostname filtering.
2019-09-04 14:30:57 -04:00
Justin Stephenson
ac612470bd Fix Recording List Column sorting in Google chrome 2019-08-30 14:36:02 -04:00
Justin Stephenson
849fcd2d49 Fix Content header CSS for PatternFly 4 2019-08-28 12:08:25 -04:00
Justin Stephenson
228645236a Update Player CSS for PatternFly 4 2019-08-26 13:17:13 -04:00
Justin Stephenson
cddcb1f40a Replace term.js with xterm.js 2019-08-26 13:17:13 -04:00
Justin Stephenson
229671f485 Journal fixes
Handle journal entries that may not contain the _EXE field.
2019-08-14 09:37:42 -04:00
Justin Stephenson
a77896f1c2 Bump Cockpit test API version 2019-08-02 10:09:15 -04:00
Justin Stephenson
3d308cd75d Fix Automated Tests 2019-08-02 09:36:58 -04:00
Justin Stephenson
fa691ce201 Tests: Avoid failure in attempting to add existing tlog user 2019-07-24 15:31:35 -04:00
Justin Stephenson
142dd4fb6a Fix hostname and username filters
Modify the default journalctl matches allowing correct behavior(logical AND)
of the appended _HOSTNAME and TLOG_USER filters.
2019-07-19 10:04:47 -04:00
Kyrylo Gliebov
e904113eff Fixed spec file 2019-05-30 14:08:11 +02:00
Kyrylo Gliebov
ffeec0bd36 Add playback time 2019-03-07 15:18:02 +01:00
Kyrylo Gliebov
66998cafa8 Add Search 2019-03-07 15:18:02 +01:00
Kyrylo Gliebov
09039778c2 LogsView bugfix 2019-03-07 15:18:02 +01:00
Kyrylo Gliebov
6f6c6b7714 Add error service 2019-03-07 15:18:02 +01:00
Kyrylo Gliebov
0ad6b11ebf Add cockpit dependency 2019-03-07 15:18:02 +01:00
Kyrylo Gliebov
5d51c45aa5 Update README.md 2019-02-18 14:44:41 +01:00
Kyrylo Gliebov
5705467b85 Add app-data-validate for metainfo.xml 2019-01-15 18:22:55 +01:00
Kyrylo Gliebov
aadc75afd0 Add xml header to metainfo.xml 2019-01-15 18:22:55 +01:00
Kyrylo Gliebov
fc7790f08b Add libappstream-glib to BuildRequires 2019-01-15 18:22:55 +01:00
Kyrylo Gliebov
776c4da012 Add summary and description to metainfo.xml 2019-01-15 18:22:55 +01:00
Kyrylo Gliebov
1b32b0c0ce Add missing localization strings 2019-01-14 13:06:58 +01:00
Kyrylo Gliebov
fff3a73253 Add tlog dependency 2019-01-14 13:06:58 +01:00
Kyrylo Gliebov
20138d3e83 Timezone bugfix 2019-01-14 13:06:58 +01:00
Kyrylo Gliebov
91877b0570 systemd-journal-remote use case update 2019-01-14 13:06:58 +01:00
Kyrylo Gliebov
2162092977 Update tests 2018-10-31 17:54:29 +01:00
Kyrylo Gliebov
67716138d2 Add testing 2018-10-30 15:44:11 +01:00
Kyrylo Gliebov
0f37e525d7 Fix Logs view 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
2fe77ec1da Fix SSSD Config 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
75e3f0f4d3 Change to React.Fragment 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
e2a6b5ee81 Switch to Slider instead of ProgressBar 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
5f58af7624 Player refactoring 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
1feb78fab7 Recording page refactoring 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
f69b9c1887 Config page refactoring 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
460b044720 Simplify Hostname 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
c51610e22e Datetimepicker refactoring 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
adc8159def Add Hostname filter conditional rendering 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
300a896483 Datetimepicker refactoring 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
39778968b3 Hostname and Username filters refactoring 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
2118f9e212 Refactoring 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
0a593d20d0 Fix journal error 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
b59140d328 Fix Config forms 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
2aa3270d97 Fix Logs View 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
1abe64fe0c Rebase and migration to full React instead of react-lite 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
4ca9b76b23 Fix URL and CSS links 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
0ce08a4420 Fix Config path 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
0852de4222 Fix InputPlayer 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
b7c21ae104 Fix CSS 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
ea3eb80c07 Add correlated Logs view 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
b1a44e337a Add Hostname filter and column 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
9c605da2f6 Add Input playback 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
471d2c160b Add SSSD config 2018-10-10 17:59:55 +02:00
Kyrylo Gliebov
a20e3c5a81 Session recording module for Cockpit initial commit 2018-10-10 17:59:55 +02:00
44 changed files with 4722 additions and 744 deletions

View file

@ -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

View file

@ -1 +0,0 @@
ghcr.io/cockpit-project/tasks:2025-07-26

View file

@ -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"]
} }
}]
} }

View file

@ -1,2 +0,0 @@
[flake8]
max-line-length = 118

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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

48
.gitignore vendored
View file

@ -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/
/tools/
# subdirs (/subdir/...)
/packaging/arch/PKGBUILD
/packaging/debian/changelog
/po/*.pot
/po/LINGUAS /po/LINGUAS
/test/common/ /tools
/test/images/
/test/static-code

View file

@ -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,

View file

@ -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

135
README.md
View file

@ -1,25 +1,33 @@
# Cockpit Starter Kit # Cockpit Session Recording
Scaffolding for a [Cockpit](https://cockpit-project.org/) module. Module for [Cockpit](http://www.cockpit-project.org) which provides session recording
configuration and playback.
It requires [tlog](https://github.com/Scribery/tlog) to record terminal sessions.
SSSD is required to manage which users / groups are recorded. Systemd Journal is used to store recordings.
Ansible role for session-recording is [here](https://github.com/nkinder/session-recording).
# Development dependencies Demos & Talks:
On Debian/Ubuntu: * [Demo 1 on YouTube](https://youtu.be/5-0WBf4rOrc)
* [Demo 2 on YouTube](https://youtu.be/Fw8g_fFvwcs)
* [FOSDEM talk](https://youtu.be/sHO5y28EHXg)
sudo apt install gettext nodejs npm make GitHub Organization:
On Fedora: * [scribery.github.io](http://scribery.github.io/)
* [Scribery](https://github.com/Scribery)
sudo dnf install gettext nodejs npm make
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)

View file

@ -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);

View 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>

View file

@ -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>

View file

@ -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"
} }
} }

View file

@ -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
}

View 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.

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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
View 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 />
);
}
}

View file

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

View file

@ -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
View 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>
);
}

View file

@ -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">

View file

@ -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 />);
}); });

View file

@ -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
View 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

File diff suppressed because it is too large Load diff

949
src/recordings.jsx Normal file
View 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
View 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
View 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;
}
}

View file

@ -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

View file

@ -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
View 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

View file

@ -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

Binary file not shown.

Binary file not shown.

View file

@ -1 +1 @@
fedora-35 fedora-36

View file

@ -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

View file

@ -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

View file

@ -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/**/*"
]
}