Compare commits

...
Sign in to create a new pull request.

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
46 changed files with 5160 additions and 1082 deletions

View file

@ -1,4 +0,0 @@
{
"presets": ["@babel/env",
"@babel/preset-react"]
}

View file

@ -1 +1,2 @@
node_modules/*
pkg/lib/*

View file

@ -1,18 +1,15 @@
{
"root": true,
"env": {
"browser": true,
"es6": true
},
"extends": ["eslint:recommended", "standard", "standard-react"],
"parser": "babel-eslint",
"extends": ["eslint:recommended", "standard", "standard-jsx", "standard-react"],
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"ecmaVersion": "2022",
"sourceType": "module"
},
"plugins": ["flowtype", "react"],
"plugins": ["flowtype", "react", "react-hooks"],
"rules": {
"indent": ["error", 4,
{
@ -22,11 +19,15 @@
"ignoredNodes": [ "JSXAttribute" ]
}],
"newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
"no-var": "error",
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
"prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }],
"react/jsx-indent": ["error", 4],
"semi": ["error", "always", { "omitLastInOneLineBlock": true }],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"camelcase": "off",
"comma-dangle": "off",
"curly": "off",
@ -38,12 +39,7 @@
"react/jsx-indent-props": "off",
"react/prop-types": "off",
"space-before-function-paren": "off",
"standard/no-callback-literal": "off",
"eqeqeq": "off",
"import/no-webpack-loader-syntax": "off",
"object-property-newline": "off",
"react/jsx-no-bind": "off"
"standard/no-callback-literal": "off"
},
"globals": {
"require": false,

1
.fmf/version Normal file
View file

@ -0,0 +1 @@
1

37
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,37 @@
# Create a GitHub upstream release
# See README.md.
name: release
on:
push:
tags:
# this is a glob, not a regexp
- '[0-9]*'
jobs:
source:
runs-on: ubuntu-latest
container:
image: ghcr.io/cockpit-project/unit-tests
options: --user root
permissions:
# create GitHub release
contents: write
steps:
- name: Clone repository
uses: actions/checkout@v3
with:
fetch-depth: 0
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
- name: Pacify git's permission check
run: git config --global --add safe.directory /__w/cockpit-session-recording/cockpit-session-recording
- name: Workaround for https://github.com/actions/checkout/pull/697
run: git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags)
- name: Build release
run: make dist
- name: Publish GitHub release
uses: cockpit-project/action-release@88d994da62d1451c7073e26748c18413fcdf46e9
with:
filename: "cockpit-session-recording-${{ github.ref_name }}.tar.xz"

8
.gitignore vendored
View file

@ -1,6 +1,6 @@
*~
*.retry
*.tar.gz
*.tar.xz
*.rpm
node_modules/
dist/
@ -8,8 +8,12 @@ dist/
/.vagrant
package-lock.json
Test*FAIL*
bots/
/bots
test/common/
test/images/
pkg
*.pot
POTFILES*
tmp/
/po/LINGUAS
/tools

38
.stylelintrc.json Normal file
View file

@ -0,0 +1,38 @@
{
"extends": "stylelint-config-standard-scss",
"rules": {
"declaration-colon-newline-after": null,
"selector-list-comma-newline-after": null,
"at-rule-empty-line-before": null,
"declaration-colon-space-before": null,
"declaration-empty-line-before": null,
"custom-property-empty-line-before": null,
"comment-empty-line-before": null,
"scss/double-slash-comment-empty-line-before": null,
"scss/dollar-variable-colon-space-after": null,
"custom-property-pattern": null,
"declaration-block-no-duplicate-properties": null,
"declaration-block-no-redundant-longhand-properties": null,
"declaration-block-no-shorthand-property-overrides": null,
"declaration-block-single-line-max-declarations": null,
"font-family-no-duplicate-names": null,
"function-url-quotes": null,
"indentation": null,
"keyframes-name-pattern": null,
"max-line-length": null,
"no-descending-specificity": null,
"no-duplicate-selectors": null,
"scss/at-extend-no-missing-placeholder": null,
"scss/at-import-partial-extension": null,
"scss/at-mixin-pattern": null,
"scss/comment-no-empty": null,
"scss/dollar-variable-pattern": null,
"scss/double-slash-comment-whitespace-inside": null,
"scss/no-global-function-names": null,
"scss/operator-no-unspaced": null,
"selector-class-pattern": null,
"selector-id-pattern": null
}
}

View file

@ -1,8 +0,0 @@
dist: trusty
sudo: false
language: node_js
node_js:
- "8"
script:
- npm install
- npm run build

208
Makefile
View file

@ -1,17 +1,45 @@
# extract name from package.json
PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' package.json)
RPM_NAME := cockpit-$(PACKAGE_NAME)
VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.')
ifeq ($(TEST_OS),)
TEST_OS = centos-7
TEST_OS = centos-8-stream
endif
export TEST_OS
TARFILE=$(RPM_NAME)-$(VERSION).tar.xz
NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz
SPEC=$(RPM_NAME).spec
PREFIX ?= /usr/local
APPSTREAMFILE=org.cockpit-project.$(PACKAGE_NAME).metainfo.xml
VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS)
# one example directory from `npm install` to check if that already ran
NODE_MODULES_TEST=node_modules/po2json
# one example file in dist/ from webpack to check if that already ran
WEBPACK_TEST=dist/index.html
# stamp file to check for node_modules/
NODE_MODULES_TEST=package-lock.json
# one example file in dist/ from bundler to check if that already ran
DIST_TEST=dist/manifest.json
# one example file in pkg/lib to check if it was already checked out
COCKPIT_REPO_STAMP=pkg/lib/cockpit-po-plugin.js
# common arguments for tar, mostly to make the generated tarballs reproducible
TAR_ARGS = --sort=name --mtime "@$(shell git show --no-patch --format='%at')" --mode=go=rX,u+rw,a-s --numeric-owner --owner=0 --group=0
all: $(WEBPACK_TEST)
all: $(DIST_TEST)
# checkout common files from Cockpit repository required to build this project;
# this has no API stability guarantee, so check out a stable tag when you start
# a new project, use the latest release, and update it from time to time
COCKPIT_REPO_FILES = \
pkg/lib \
test/common \
$(NULL)
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
COCKPIT_REPO_COMMIT = 9c73bec7e1dc2395a00aa0c510fd7210b6c96a16 # 300.1 + 42 commits
$(COCKPIT_REPO_FILES): $(COCKPIT_REPO_STAMP)
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
$(COCKPIT_REPO_STAMP): Makefile
@git rev-list --quiet --objects $(COCKPIT_REPO_TREE) -- 2>/dev/null || \
git fetch --no-tags --no-write-fetch-head --depth=1 $(COCKPIT_REPO_URL) $(COCKPIT_REPO_COMMIT)
git archive $(COCKPIT_REPO_TREE) -- $(COCKPIT_REPO_FILES) | tar x
#
# i18n
@ -19,89 +47,98 @@ all: $(WEBPACK_TEST)
LINGUAS=$(basename $(notdir $(wildcard po/*.po)))
po/POTFILES.js.in:
mkdir -p $(dir $@)
find src/ -name '*.js' -o -name '*.jsx' -o -name '*.es6' > $@
po/$(PACKAGE_NAME).js.pot: po/POTFILES.js.in
xgettext --default-domain=cockpit --output=$@ --language=C --keyword= \
--keyword=_:1,1t --keyword=_:1c,2,1t --keyword=C_:1c,2 \
po/$(PACKAGE_NAME).js.pot:
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ --language=C --keyword= \
--keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
--keyword=N_ --keyword=NC_:1c,2 \
--keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
--keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
--keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
--from-code=UTF-8 --files-from=$^
--from-code=UTF-8 $$(find src/ -name '*.js' -o -name '*.jsx')
po/POTFILES.html.in:
mkdir -p $(dir $@)
find src -name '*.html' > $@
po/$(PACKAGE_NAME).html.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
pkg/lib/html2po.js -o $@ $$(find src -name '*.html')
po/$(PACKAGE_NAME).html.pot: po/POTFILES.html.in
po/html2po -f $^ -o $@
po/$(PACKAGE_NAME).manifest.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
pkg/lib/manifest2po.js src/manifest.json -o $@
po/$(PACKAGE_NAME).manifest.pot:
po/manifest2po src/manifest.json -o $@
po/$(PACKAGE_NAME).metainfo.pot: $(APPSTREAMFILE)
xgettext --default-domain=$(PACKAGE_NAME) --output=$@ $<
po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot
po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot po/$(PACKAGE_NAME).metainfo.pot
msgcat --sort-output --output-file=$@ $^
# Update translations against current PO template
update-po: po/$(PACKAGE_NAME).pot
for lang in $(LINGUAS); do \
msgmerge --output-file=po/$$lang.po po/$$lang.po $<; \
done
dist/po.%.js: po/%.po $(NODE_MODULES_TEST)
mkdir -p $(dir $@)
po/po2json -m po/po.empty.js -o $@.js.tmp $<
mv $@.js.tmp $@
po/LINGUAS:
echo $(LINGUAS) | tr ' ' '\n' > $@
#
# Build/Install/dist
#
%.spec: %.spec.in
sed -e 's/@VERSION@/$(VERSION)/g' $< > $@
$(SPEC): packaging/$(SPEC).in $(NODE_MODULES_TEST)
provides=$$(npm ls --omit dev --package-lock-only --depth=Infinity | grep -Eo '[^[:space:]]+@[^[:space:]]+' | sort -u | sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /'); \
awk -v p="$$provides" '{gsub(/%{VERSION}/, "$(VERSION)"); gsub(/%{NPM_PROVIDES}/, p)}1' $< > $@
$(WEBPACK_TEST): $(NODE_MODULES_TEST) $(shell find src/ -type f) package.json webpack.config.js $(patsubst %,dist/po.%.js,$(LINGUAS))
NODE_ENV=$(NODE_ENV) npm run build
$(DIST_TEST): $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js
NODE_ENV=$(NODE_ENV) ./build.js
watch: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
NODE_ENV=$(NODE_ENV) ./build.js --watch
clean:
rm -rf dist/
[ ! -e cockpit-$(PACKAGE_NAME).spec.in ] || rm -f cockpit-$(PACKAGE_NAME).spec
rm -f $(SPEC)
rm -f po/LINGUAS
install: $(WEBPACK_TEST)
mkdir -p $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME)
cp -r dist/* $(DESTDIR)/usr/share/cockpit/$(PACKAGE_NAME)
mkdir -p $(DESTDIR)/usr/share/metainfo/
cp org.cockpit-project.$(PACKAGE_NAME).metainfo.xml $(DESTDIR)/usr/share/metainfo/
install: $(DIST_TEST) po/LINGUAS
mkdir -p $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME)
cp -r dist/* $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME)
mkdir -p $(DESTDIR)$(PREFIX)/share/metainfo/
msgfmt --xml -d po \
--template $(APPSTREAMFILE) \
-o $(DESTDIR)$(PREFIX)/share/metainfo/$(APPSTREAMFILE)
# this requires a built source tree and avoids having to install anything system-wide
devel-install: $(WEBPACK_TEST)
devel-install: $(DIST_TEST)
mkdir -p ~/.local/share/cockpit
ln -s `pwd`/dist ~/.local/share/cockpit/$(PACKAGE_NAME)
# when building a distribution tarball, call webpack with a 'production' environment
# ship a stub node_modules/ so that `make` works without re-running `npm install`
dist-gzip: NODE_ENV=production
dist-gzip: all cockpit-$(PACKAGE_NAME).spec
mv node_modules node_modules.release
mkdir -p $(NODE_MODULES_TEST)
touch -r package.json $(NODE_MODULES_TEST)
touch dist/*
tar czf cockpit-$(PACKAGE_NAME)-$(VERSION).tar.gz --transform 's,^,cockpit-$(PACKAGE_NAME)/,' \
--exclude cockpit-$(PACKAGE_NAME).spec.in \
$$(git ls-files) cockpit-$(PACKAGE_NAME).spec dist/ node_modules
rm -rf node_modules
mv node_modules.release node_modules
# assumes that there was symlink set up using the above devel-install target,
# and removes it
devel-uninstall:
rm -f ~/.local/share/cockpit/$(PACKAGE_NAME)
srpm: dist-gzip cockpit-$(PACKAGE_NAME).spec
print-version:
@echo "$(VERSION)"
dist: $(TARFILE)
@ls -1 $(TARFILE)
# when building a distribution tarball, call bundler with a 'production' environment
# we don't ship node_modules for license and compactness reasons; we ship a
# pre-built dist/ (so it's not necessary) and ship package-lock.json (so that
# node_modules/ can be reconstructed if necessary)
$(TARFILE): export NODE_ENV=production
$(TARFILE): $(DIST_TEST) $(SPEC)
if type appstream-util >/dev/null 2>&1; then appstream-util validate-relax --nonet *.metainfo.xml; fi
tar --xz $(TAR_ARGS) -cf $(TARFILE) --transform 's,^,$(RPM_NAME)/,' \
--exclude packaging/$(SPEC).in --exclude node_modules \
$$(git ls-files) $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) $(SPEC) dist/
$(NODE_CACHE): $(NODE_MODULES_TEST)
tar --xz $(TAR_ARGS) -cf $@ node_modules
node-cache: $(NODE_CACHE)
# convenience target for developers
srpm: $(TARFILE) $(NODE_CACHE) $(SPEC)
rpmbuild -bs \
--define "_sourcedir `pwd`" \
--define "_srcrpmdir `pwd`" \
cockpit-$(PACKAGE_NAME).spec
$(SPEC)
rpm: dist-gzip cockpit-$(PACKAGE_NAME).spec
# convenience target for developers
rpm: $(TARFILE) $(NODE_CACHE) $(SPEC)
mkdir -p "`pwd`/output"
mkdir -p "`pwd`/rpmbuild"
rpmbuild -bb \
@ -111,39 +148,46 @@ rpm: dist-gzip cockpit-$(PACKAGE_NAME).spec
--define "_srcrpmdir `pwd`" \
--define "_rpmdir `pwd`/output" \
--define "_buildrootdir `pwd`/build" \
cockpit-$(PACKAGE_NAME).spec
$(SPEC)
find `pwd`/output -name '*.rpm' -printf '%f\n' -exec mv {} . \;
rm -r "`pwd`/rpmbuild"
rm -r "`pwd`/output" "`pwd`/build"
# build a VM with locally built rpm installed
$(VM_IMAGE): rpm bots
rm -f $(VM_IMAGE) $(VM_IMAGE).qcow2
bots/image-customize -v -i cockpit -i `pwd`/cockpit-$(PACKAGE_NAME)-*.noarch.rpm -s $(CURDIR)/test/vm.install $(TEST_OS)
# build a VM with locally built distro pkgs installed
# disable networking, VM images have mock/pbuilder with the common build dependencies pre-installed
$(VM_IMAGE): $(TARFILE) $(NODE_CACHE) bots test/vm.install
bots/image-customize --fresh \
--upload $(NODE_CACHE):/var/tmp/ --build $(TARFILE) \
--upload ./test/files/1.journal:/var/log/journal/1.journal \
--upload ./test/files/binary-rec.journal:/var/log/journal/binary-rec.journal \
--script $(CURDIR)/test/vm.install $(TEST_OS)
# convenience target for the above
vm: $(VM_IMAGE)
echo $(VM_IMAGE)
@echo $(VM_IMAGE)
# run the browser integration tests; skip check for SELinux denials
check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common
TEST_AUDIT_NO_SELINUX=1 test/check-application
# convenience target to print the filename of the test image
print-vm:
@echo $(VM_IMAGE)
# checkout Cockpit's bots/ directory for standard test VM images and API to launch them
# must be from cockpit's master, as only that has current and existing images; but testvm.py API is stable
bots:
git fetch --depth=1 https://github.com/cockpit-project/cockpit.git
git checkout --force FETCH_HEAD -- bots/
git reset bots
# convenience target to setup all the bits needed for the integration tests
# without actually running them
prepare-check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common
# checkout Cockpit's test API; this has no API stability guarantee, so check out a stable tag
# when you start a new project, use the latest relese, and update it from time to time
test/common:
git fetch --depth=1 https://github.com/cockpit-project/cockpit.git 176
git checkout --force FETCH_HEAD -- test/common
git reset test/common
# run the browser integration tests
# this will run all tests/check-* and format them as TAP
check: prepare-check
test/common/run-tests ${RUN_TESTS_OPTIONS}
# checkout Cockpit's bots for standard test VM images and API to launch them
bots: $(COCKPIT_REPO_STAMP)
test/common/make-bots
$(NODE_MODULES_TEST): package.json
npm install
# if it exists already, npm install won't update it; force that so that we always get up-to-date packages
rm -f package-lock.json
# unset NODE_ENV, skips devDependencies otherwise
env -u NODE_ENV npm install --ignore-scripts
env -u NODE_ENV npm prune
.PHONY: all clean install devel-install dist-gzip srpm rpm check vm update-po
.PHONY: all clean install devel-install devel-uninstall print-version dist node-cache rpm prepare-check check vm print-vm

142
README.md
View file

@ -1,6 +1,24 @@
# Cockpit Starter Kit
# Cockpit Session Recording
Scaffolding for a [Cockpit](http://www.cockpit-project.org) module.
Module for [Cockpit](http://www.cockpit-project.org) which provides session recording
configuration and playback.
It requires [tlog](https://github.com/Scribery/tlog) to record terminal sessions.
SSSD is required to manage which users / groups are recorded. Systemd Journal is used to store recordings.
Ansible role for session-recording is [here](https://github.com/nkinder/session-recording).
Demos & Talks:
* [Demo 1 on YouTube](https://youtu.be/5-0WBf4rOrc)
* [Demo 2 on YouTube](https://youtu.be/Fw8g_fFvwcs)
* [FOSDEM talk](https://youtu.be/sHO5y28EHXg)
GitHub Organization:
* [scribery.github.io](http://scribery.github.io/)
* [Scribery](https://github.com/Scribery)
This project is based on the [Cockpit Starter Kit](https://github.com/cockpit-project/starter-kit).
See [Starter Kit Intro](http://cockpit-project.org/blog/cockpit-starter-kit.html) for details.
# Getting and building the source
@ -8,37 +26,66 @@ Make sure you have `npm` available (usually from your distribution package).
These commands check out the source and build it into the `dist/` directory:
```
git clone https://github.com/cockpit-project/starter-kit.git
cd starter-kit
git clone https://github.com/Scribery/cockpit-session-recording.git
cd cockpit-session-recording
make
```
# Installing
`make install` compiles and installs the package in `/usr/share/cockpit/`. The
`make install` compiles and installs the package in `/usr/local/share/cockpit/`. The
convenience targets `srpm` and `rpm` build the source and binary rpms,
respectively. Both of these make use of the `dist-gzip` target, which is used
respectively. Both of these make use of the `dist` target, which is used
to generate the distribution tarball. In `production` mode, source files are
automatically minified and compressed. Set `NODE_ENV=production` if you want to
duplicate this behavior.
For development, you usually want to run your module straight out of the git
tree. To do that, link that to the location were `cockpit-bridge` looks for packages:
tree. To do that, run `make devel-install`, which links your checkout to the
location were cockpit-bridge looks for packages. If you prefer to do
this manually:
```
mkdir -p ~/.local/share/cockpit
ln -s `pwd`/dist ~/.local/share/cockpit/starter-kit
ln -s `pwd`/dist ~/.local/share/cockpit/session-recording
```
After changing the code and running `make` again, reload the Cockpit page in
your browser.
You can also use
[watch mode](https://esbuild.github.io/api/#watch) to
automatically update the bundle on every code change with
$ ./build.js -w
or
$ make watch
When developing against a virtual machine, watch mode can also automatically upload
the code changes by setting the `RSYNC` environment variable to
the remote hostname.
$ RSYNC=c make watch
When developing against a remote host as a normal user, `RSYNC_DEVEL` can be
set to upload code changes to `~/.local/share/cockpit/` instead of
`/usr/local`.
$ RSYNC_DEVEL=example.com make watch
To "uninstall" the locally installed version, run `make devel-uninstall`, or
remove manually the symlink:
rm ~/.local/share/cockpit/starter-kit
# Running eslint
Cockpit Starter Kit uses [ESLint](https://eslint.org/) to automatically check
JavaScript code style in `.jsx` and `.es6` files.
JavaScript code style in `.js` and `.jsx` files.
The linter is executed within every build as a webpack preloader.
eslint is executed within every build.
For developer convenience, the ESLint can be started explicitly by:
@ -50,60 +97,49 @@ Violations of some rules can be fixed automatically by:
Rules configuration can be found in the `.eslintrc.json` file.
# Automated Testing
## Running stylelint
Cockpit uses [Stylelint](https://stylelint.io/) to automatically check CSS code
style in `.css` and `scss` files.
styleint is executed within every build.
For developer convenience, the Stylelint can be started explicitly by:
$ npm run stylelint
Violations of some rules can be fixed automatically by:
$ npm run stylelint:fix
Rules configuration can be found in the `.stylelintrc.json` file.
During fast iterative development, you can also choose to not run eslint/stylelint.
This speeds up the build and avoids build failures due to e. g. ill-formatted
css or other issues:
$ ./build.js -es
# Running tests locally
Run `make check` to build an RPM, install it into a standard Cockpit test VM
(centos-7 by default), and run the test/check-application integration test on
(centos-8-stream by default), and run the test/check-application integration test on
it. This uses Cockpit's Chrome DevTools Protocol based browser tests, through a
Python API abstraction. Note that this API is not guaranteed to be stable, so
if you run into failures and don't want to adjust tests, consider checking out
Cockpit's test/common from a tag instead of master (see the `test/common`
Cockpit's test/common from a tag instead of main (see the `test/common`
target in `Makefile`).
After the test VM is prepared, you can manually run the test without rebuilding
the VM, possibly with extra options for tracing and halting on test failures
(for interactive debugging):
TEST_OS=centos-7 test/check-application -tvs
TEST_OS=centos-8-stream test/check-application -tvs
It is possible to setup the test environment without running the tests:
TEST_OS=centos-8-stream make prepare-check
You can also run the test against a different Cockpit image, for example:
TEST_OS=fedora-28 make check
# Vagrant
This directory contains a Vagrantfile that installs and starts cockpit on a
Fedora 26 cloud image. Run `vagrant up` to start it and `vagrant rsync` to
synchronize the `dist` directory to `/usr/local/share/cockit/starter-kit`. Use
`vagrant rsync-auto` to automatically sync when contents of the `dist`
directory change.
# Customizing
After cloning the Starter Kit you should rename the files, package names, and
labels to your own project's name. Use these commands to find out what to
change:
find -iname '*starter*'
git grep -i starter
# Automated release
Once your cloned project is ready for a release, you should consider automating
that. [Cockpituous release](https://github.com/cockpit-project/cockpituous/tree/master/release)
aims to fully automate project releases to GitHub, Fedora, Ubuntu, COPR, Docker
Hub, and other places. The intention is that the only manual step for releasing
a project is to create a signed tag for the version number; pushing the tag
then triggers a GitHub webhook that calls a set of release scripts (on
Cockpit's CI infrastructure).
starter-kit includes an example [cockpitous release script](./cockpituous-release)
that builds an upstream release tarball and source RPM. Please see the above
cockpituous documentation for details.
# Further reading
* The [Starter Kit announcement](http://cockpit-project.org/blog/cockpit-starter-kit.html)
blog post explains the rationale for this project.
* [Cockpit Deployment and Developer documentation](http://cockpit-project.org/guide/latest/)
* [Make your project easily discoverable](http://cockpit-project.org/blog/making-a-cockpit-application.html)
TEST_OS=fedora-34 make check

30
Vagrantfile vendored
View file

@ -1,30 +0,0 @@
Vagrant.configure(2) do |config|
config.vm.box = "fedora/28-cloud-base"
config.vm.network "forwarded_port", guest: 9090, host: 9090
if Dir.glob("dist/*").length == 0
config.vm.post_up_message = "NOTE: Distribution directory is empty. Run `make` to see your module show up in cockpit"
end
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.synced_folder "dist/", "/usr/local/share/cockpit/" + File.basename(Dir.pwd), type: "rsync", create: true
config.vm.provider "libvirt" do |libvirt|
libvirt.memory = 1024
end
config.vm.provider "virtualbox" do |virtualbox|
virtualbox.memory = 1024
end
config.vm.provision "shell", inline: <<-EOF
set -eu
sudo dnf install -y cockpit
printf "[WebService]\nAllowUnencrypted=true\n" > /etc/cockpit/cockpit.conf
systemctl enable cockpit.socket
systemctl start cockpit.socket
EOF
end

140
build.js Executable file
View file

@ -0,0 +1,140 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import copy from 'esbuild-plugin-copy';
import { cleanPlugin } from './pkg/lib/esbuild-cleanup-plugin.js';
import { cockpitCompressPlugin } from './pkg/lib/esbuild-compress-plugin.js';
import { cockpitPoEsbuildPlugin } from './pkg/lib/cockpit-po-plugin.js';
import { cockpitRsyncEsbuildPlugin } from './pkg/lib/cockpit-rsync-plugin.js';
import { esbuildStylesPlugins } from './pkg/lib/esbuild-common.js';
import { eslintPlugin } from './pkg/lib/esbuild-eslint-plugin.js';
import { stylelintPlugin } from './pkg/lib/esbuild-stylelint-plugin.js';
const production = process.env.NODE_ENV === 'production';
const useWasm = os.arch() !== 'x64';
const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default;
const lintDefault = process.env.LINT ? process.env.LINT === '0' : production;
const parser = (await import('argparse')).default.ArgumentParser();
parser.add_argument('-r', '--rsync', { help: "rsync bundles to ssh target after build", metavar: "HOST" });
parser.add_argument('-w', '--watch', { action: 'store_true', help: "Enable watch mode", default: process.env.ESBUILD_WATCH === "true" });
parser.add_argument('-e', '--no-eslint', { action: 'store_true', help: "Disable eslint linting", default: lintDefault });
parser.add_argument('-s', '--no-stylelint', { action: 'store_true', help: "Disable stylelint linting", default: lintDefault });
const args = parser.parse_args();
if (args.rsync)
process.env.RSYNC = args.rsync;
// List of directories to use when using import statements
const nodePaths = ['pkg/lib'];
const outdir = 'dist';
// Obtain package name from package.json
const packageJson = JSON.parse(fs.readFileSync('package.json'));
function notifyEndPlugin() {
return {
name: 'notify-end',
setup(build) {
let startTime;
build.onStart(() => {
startTime = new Date();
});
build.onEnd(() => {
const endTime = new Date();
const timeStamp = endTime.toTimeString().split(' ')[0];
console.log(`${timeStamp}: Build finished in ${endTime - startTime} ms`);
});
}
};
}
const cwd = process.cwd();
// similar to fs.watch(), but recursively watches all subdirectories
function watch_dirs(dir, on_change) {
const callback = (ev, dir, fname) => {
// only listen for "change" events, as renames are noisy
// ignore hidden files
const isHidden = /^\./.test(fname);
if (ev !== "change" || isHidden) {
return;
}
on_change(path.join(dir, fname));
};
fs.watch(dir, {}, (ev, path) => callback(ev, dir, path));
// watch all subdirectories in dir
const d = fs.opendirSync(dir);
let dirent;
while ((dirent = d.readSync()) !== null) {
if (dirent.isDirectory())
watch_dirs(path.join(dir, dirent.name), on_change);
}
d.closeSync();
}
const context = await esbuild.context({
...!production ? { sourcemap: "linked" } : {},
bundle: true,
entryPoints: ['./src/index.js'],
external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts
legalComments: 'external', // Move all legal comments to a .LEGAL.txt file
loader: { ".js": "jsx" },
minify: production,
nodePaths,
outdir,
target: ['es2020'],
plugins: [
cleanPlugin(),
...args.no_stylelint ? [] : [stylelintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(css?|scss?)$') })],
...args.no_eslint ? [] : [eslintPlugin({ filter: new RegExp(cwd + '\/src\/.*\.(jsx?|js?)$') })],
// Esbuild will only copy assets that are explicitly imported and used
// in the code. This is a problem for index.html and manifest.json which are not imported
copy({
assets: [
{ from: ['./src/manifest.json'], to: ['./manifest.json'] },
{ from: ['./src/index.html'], to: ['./index.html'] },
]
}),
...esbuildStylesPlugins,
cockpitPoEsbuildPlugin(),
...production ? [cockpitCompressPlugin()] : [],
cockpitRsyncEsbuildPlugin({ dest: packageJson.name }),
notifyEndPlugin(),
]
});
try {
await context.rebuild();
} catch (e) {
if (!args.watch)
process.exit(1);
// ignore errors in watch mode
}
if (args.watch) {
const on_change = async path => {
console.log("change detected:", path);
await context.cancel();
try {
await context.rebuild();
} catch (e) {} // ignore in watch mode
};
watch_dirs('src', on_change);
// wait forever until Control-C
await new Promise(() => {});
}
context.dispose();

View file

@ -1,25 +0,0 @@
Name: cockpit-starter-kit
Version: @VERSION@
Release: 1%{?dist}
Summary: Cockpit Starter Kit Example Module
License: LGPLv2+
Source: cockpit-starter-kit-%{version}.tar.gz
BuildArch: noarch
%define debug_package %{nil}
%description
Cockpit Starter Kit Example Module
%prep
%setup -n cockpit-starter-kit
%install
%make_install
%files
%{_datadir}/cockpit/*
%{_datadir}/metainfo/*
%changelog

View file

@ -1,31 +0,0 @@
# This is a script run to release welder-web through Cockpituous:
# https://github.com/cockpit-project/cockpituous/tree/master/release
# Anything that start with 'job' may run in a way that it SIGSTOP's
# itself when preliminary preparition and then gets a SIGCONT in
# order to complete its work.
#
# Check cockpituous documentation for available release targets.
RELEASE_SOURCE="_release/source"
RELEASE_SPEC="cockpit-starter-kit.spec"
RELEASE_SRPM="_release/srpm"
job release-source
job release-srpm
# Once you have a Fedora package and add the https://pagure.io/user/cockpit
# user to your project's maintainers, you can also upload to Fedora automatically:
## Authenticate for pushing into Fedora dist-git (works in Cockpituous release container)
# cat ~/.fedora-password | kinit cockpit@FEDORAPROJECT.ORG
## Do fedora builds for the tag, using tarball
# job release-koji -k master
# job release-koji f29
# job release-bodhi F29
# These are likely the first of your release targets; but run them after Fedora uploads,
# so that failures there will fail the release early, before publishing on GitHub
# job release-github
# job release-copr @myorg/myrepo

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,15 +0,0 @@
<component type="addon">
<id>org.cockpit-project.starter-kit</id>
<metadata_license>CC0-1.0</metadata_license>
<name>Starter Kit</name>
<summary>
Scaffolding for a cockpit module.
</summary>
<description>
<p>
Scaffolding for a cockpit module.
</p>
</description>
<extends>cockpit.desktop</extends>
<launchable type="cockpit-manifest">cockpit-starter-kit</launchable>
</component>

View file

@ -1,50 +1,67 @@
{
"name": "starter-kit",
"version": "0.1.0",
"description": "Scaffolding for a cockpit module",
"name": "session-recording",
"description": "Module for Cockpit which provides session recording configuration and playback",
"type": "module",
"main": "index.js",
"repository": "git@github.com:cockpit/starter-kit.git",
"repository": "git@github.com:Scribery/cockpit-session-recording.git",
"author": "",
"license": "LGPL-2.1",
"engines": {
"node": ">= 16"
},
"scripts": {
"build": "webpack",
"eslint": "eslint --ext .jsx --ext .es6 src/",
"eslint:fix": "eslint --fix --ext .jsx --ext .es6 src/"
"watch": "ESBUILD_WATCH='true' ./build.js",
"build": "./build.js",
"eslint": "eslint --ext .js --ext .jsx src/",
"eslint:fix": "eslint --fix --ext .js --ext .jsx src/",
"stylelint": "stylelint src/*{.css,scss}",
"stylelint:fix": "stylelint --fix src/*{.css,scss}"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.0",
"chrome-remote-interface": "^0.25.5",
"compression-webpack-plugin": "^1.1.11",
"copy-webpack-plugin": "^4.5.2",
"css-loader": "^0.28.11",
"eslint": "^5.4.0",
"eslint-config-standard": "^11.0.0",
"eslint-config-standard-react": "^6.0.0",
"eslint-loader": "^2.1.0",
"eslint-plugin-flowtype": "^2.50.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^6.9.0",
"eslint-plugin-standard": "^3.1.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"argparse": "^2.0.1",
"chrome-remote-interface": "^0.32.1",
"esbuild": "0.18.6",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-replace": "^1.3.0",
"esbuild-sass-plugin": "2.10.0",
"esbuild-wasm": "^0.18.6",
"eslint": "^8.13.0",
"eslint-config-standard": "^17.0.0-1",
"eslint-config-standard-jsx": "^11.0.0-1",
"eslint-config-standard-react": "^13.0.0",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"gettext-parser": "2.0.0",
"htmlparser": "^1.7.7",
"jed": "^1.1.1",
"po2json": "^0.4.5",
"sass-loader": "^7.0.3",
"qunit": "^2.9.3",
"sass": "^1.61.0",
"sizzle": "^2.3.3",
"stdio": "^0.2.7",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0"
"stylelint": "^15.10.1",
"stylelint-config-standard": "^34.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-formatter-pretty": "^3.2.0"
},
"dependencies": {
"@babel/polyfill": "^7.0.0",
"node-sass": "^4.9.0",
"react": "^16.4.2",
"react-dom": "^16.4.2"
"@patternfly/patternfly": "5.0.4",
"@patternfly/react-core": "5.0.1",
"@patternfly/react-icons": "5.0.1",
"@patternfly/react-styles": "5.0.1",
"@patternfly/react-table": "5.0.1",
"@patternfly/react-tokens": "5.0.1",
"buffer": "^6.0.3",
"comment-json": "^4.2.3",
"date-fns": "^2.29.3",
"ini": "^4.1.0",
"jquery": "^3.6.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"throttle-debounce": "^5.0.0",
"xterm": "5.1.0",
"xterm-addon-canvas": "^0.4.0"
}
}

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.

70
packit.yaml Normal file
View file

@ -0,0 +1,70 @@
# Enable RPM builds and running integration tests in PRs through https://packit.dev/
# To use this, enable Packit-as-a-service in GitHub: https://packit.dev/docs/packit-as-a-service/
# See https://packit.dev/docs/configuration/ for the format of this file
#
upstream_project_url: https://github.com/Scribery/cockpit-session-recording
# enable notification of failed downstream jobs as issues
issue_repository: https://github.com/Scribery/cockpit-session-recording
specfile_path: cockpit-session-recording.spec
upstream_package_name: cockpit-session-recording
downstream_package_name: cockpit-session-recording
# use the nicely formatted release description from our upstream release, instead of git shortlog
copy_upstream_release_description: true
srpm_build_deps:
- make
- nodejs-npm
actions:
post-upstream-clone:
- make cockpit-session-recording.spec
create-archive: make dist
# starter-kit.git has no release tags; your project can drop this once you have a release
get-current-version: make print-version
jobs:
- job: copr_build
trigger: pull_request
targets:
- fedora-all
- fedora-latest-aarch64
- centos-stream-8
- centos-stream-9
- centos-stream-9-aarch64
- job: tests
trigger: pull_request
targets:
- fedora-all
- fedora-latest-aarch64
- centos-stream-8
- centos-stream-9
- centos-stream-9-aarch64
# Build releases in COPR: https://packit.dev/docs/configuration/#copr_build
#- job: copr_build
# trigger: release
# owner: your_copr_login
# project: your_copr_project
# preserve_project: True
# targets:
# - fedora-all
# - centos-stream-9-x86_64
# Build releases in Fedora: https://packit.dev/docs/configuration/#propose_downstream
- job: propose_downstream
trigger: release
dist_git_branches:
- fedora-all
- job: koji_build
trigger: commit
dist_git_branches:
- fedora-all
- job: bodhi_update
trigger: commit
dist_git_branches:
# rawhide updates are created automatically
- fedora-branched

10
plans/all.fmf Normal file
View file

@ -0,0 +1,10 @@
summary:
Run all tests
discover:
how: fmf
execute:
how: tmt
# Let's handle them upstream only, don't break Fedora/RHEL reverse dependency gating
environment:
TEST_AUDIT_NO_SELINUX: 1

View file

@ -4,27 +4,20 @@ msgid ""
msgstr ""
"Project-Id-Version: starter-kit 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-29 00:14+0200\n"
"POT-Creation-Date: 2022-03-09 16:09+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
#: src/index.html:20
msgid "Cockpit Starter Kit"
msgstr "Cockpit Bausatz"
#: src/app.jsx:42
#: src/app.jsx:43
msgid "Running on $0"
msgstr "Läuft auf $0"
#: src/manifest.json
msgid "Starter Kit"
msgstr "Bausatz"
#: src/app.jsx:29
msgid "Unknown"
msgstr "Unbekannt"

View file

@ -1,264 +0,0 @@
#!/usr/bin/env node
/*
* Extracts translatable strings from HTML files in the following forms:
*
* <tag translate>String</tag>
* <tag translate context="value">String</tag>
* <tag translate="...">String</tag>
* <tag translate-attr attr="String"></tag>
*
* Supports the following Glade compatible forms:
*
* <tag translatable="yes">String</tag>
* <tag translatable="yes" context="value">String</tag>
*
* Supports the following angular-gettext compatible forms:
*
* <translate>String</translate>
* <tag translate-plural="Plural">Singular</tag>
*
* Note that some of the use of the translated may not support all the strings
* depending on the code actually using these strings to translate the HTML.
*/
function fatal(message, code) {
console.log((filename || "html2po") + ": " + message);
process.exit(code || 1);
}
function usage() {
console.log("usage: html2po input output");
process.exit(2);
}
var fs, htmlparser, path, stdio;
try {
fs = require('fs');
path = require('path');
htmlparser = require('htmlparser');
stdio = require('stdio');
} catch (ex) {
fatal(ex.message, 127); /* missing looks for this */
}
var opts = stdio.getopt({
directory: { key: "d", args: 1, description: "Base directory for input files" },
output: { key: "o", args: 1, description: "Output file" },
from: { key: "f", args: 1, description: "File containing list of input files" },
});
if (!opts.from && opts.args.length < 1) {
usage();
}
var input = opts.args;
var entries = { };
/* Filename being parsed and offset of line number */
var filename = null;
var offsets = 0;
/* The HTML parser we're using */
var handler = new htmlparser.DefaultHandler(function(error, dom) {
if (error)
fatal(error);
else
walk(dom);
});
prepare();
/* Decide what input files to process */
function prepare() {
if (opts.from) {
fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) {
if (err)
fatal(err.message);
input = data.split("\n").filter(function(value) {
return !!value;
}).concat(input);
step();
});
} else {
step();
}
}
/* Now process each file in turn */
function step() {
filename = input.shift();
if (filename === undefined) {
finish();
return;
}
/* Qualify the filename if necessary */
var full = filename;
if (opts.directory)
full = path.join(opts.directory, filename);
fs.readFile(full, { encoding: "utf-8"}, function(err, data) {
if (err)
fatal(err.message);
var parser = new htmlparser.Parser(handler, { includeLocation: true });
parser.parseComplete(data);
step();
});
}
/* Process an array of nodes */
function walk(children) {
if (!children)
return;
children.forEach(function(child) {
var line = (child.location || { }).line || 0;
var offset = line - 1;
/* Scripts get their text processed as HTML */
if (child.type == 'script' && child.children) {
var parser = new htmlparser.Parser(handler, { includeLocation: true });
/* Make note of how far into the outer HTML file we are */
offsets += offset;
child.children.forEach(function(node) {
parser.parseChunk(node.raw);
});
parser.done();
offsets -= offset;
/* Tags get extracted as usual */
} else if (child.type == 'tag') {
tag(child);
}
});
}
/* Process a single loaded tag */
function tag(node) {
var tasks, line, entry;
var attrs = node.attribs || { };
var nest = true;
/* Extract translate strings */
if ("translate" in attrs || "translatable" in attrs) {
tasks = (attrs["translate"] || attrs["translatable"] || "yes").split(" ");
/* Calculate the line location taking into account nested parsing */
line = (node.location || { })["line"] || 0;
line += offsets;
entry = {
msgctxt: attrs['translate-context'] || attrs['context'],
msgid_plural: attrs['translate-plural'],
locations: [ filename + ":" + line ]
};
/* For each thing listed */
tasks.forEach(function(task) {
var copy = Object.assign({}, entry);
/* The element text itself */
if (task == "yes" || task == "translate") {
copy.msgid = extract(node.children);
nest = false;
/* An attribute */
} else if (task) {
copy.msgid = attrs[task];
}
if (copy.msgid)
push(copy);
});
}
/* Walk through all the children */
if (nest)
walk(node.children);
}
/* Push an entry onto the list */
function push(entry) {
var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
var prev = entries[key];
if (prev) {
prev.locations = prev.locations.concat(entry.locations);
} else {
entries[key] = entry;
}
}
/* Extract the given text */
function extract(children) {
if (!children)
return null;
var i, len, node, str = [];
children.forEach(function(node) {
if (node.type == 'tag' && node.children)
str.push(extract(node.children))
else if (node.type == 'text' && node.data)
str.push(node.data);
});
return str.join("");
}
/* Escape a string for inclusion in po file */
function escape(string) {
var bs = string.split('\\').join('\\\\').split('"').join('\\"');
return bs.split("\n").map(function(line) {
return '"' + line + '"';
}).join("\n");
}
/* Finish by writing out the strings */
function finish() {
var result = [
'msgid ""',
'msgstr ""',
'"Project-Id-Version: PACKAGE_VERSION\\n"',
'"MIME-Version: 1.0\\n"',
'"Content-Type: text/plain; charset=UTF-8\\n"',
'"Content-Transfer-Encoding: 8bit\\n"',
'"X-Generator: Cockpit html2po\\n"',
'',
];
var msgid, entry;
for (msgid in entries) {
entry = entries[msgid];
result.push('#: ' + entry.locations.join(" "));
if (entry.msgctxt)
result.push('msgctxt ' + escape(entry.msgctxt));
result.push('msgid ' + escape(entry.msgid));
if (entry.msgid_plural) {
result.push('msgid_plural ' + escape(entry.msgid_plural));
result.push('msgstr[0] ""');
result.push('msgstr[1] ""');
} else {
result.push('msgstr ""');
}
result.push('');
}
var data = result.join('\n');
if (!opts.output) {
process.stdout.write(data);
process.exit(0);
} else {
fs.writeFile(opts.output, data, function(err) {
if (err)
fatal(err.message);
process.exit(0);
});
}
}

View file

@ -1,161 +0,0 @@
#!/usr/bin/env node
/*
* Extracts translatable strings from manifest.json files.
*
*/
function fatal(message, code) {
console.log((filename || "manifest2po") + ": " + message);
process.exit(code || 1);
}
function usage() {
console.log("usage: manifest2po [-o output] input...");
process.exit(2);
}
var fs, path, stdio;
try {
fs = require('fs');
path = require('path');
stdio = require('stdio');
} catch (ex) {
fatal(ex.message, 127); /* missing looks for this */
}
var opts = stdio.getopt({
directory: { key: "d", args: 1, description: "Base directory for input files" },
output: { key: "o", args: 1, description: "Output file" },
from: { key: "f", args: 1, description: "File containing list of input files" },
});
if (!opts.from && opts.args.length < 1) {
usage();
}
var input = opts.args;
var entries = { };
/* Filename being parsed */
var filename = null;
prepare();
/* Decide what input files to process */
function prepare() {
if (opts.from) {
fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) {
if (err)
fatal(err.message);
input = data.split("\n").filter(function(value) {
return !!value;
}).concat(input);
step();
});
} else {
step();
}
}
/* Now process each file in turn */
function step() {
filename = input.shift();
if (filename === undefined) {
finish();
return;
}
if (path.basename(filename) != "manifest.json")
return step();
fs.readFile(filename, { encoding: "utf-8"}, function(err, data) {
if (err)
fatal(err.message);
process_manifest(JSON.parse(data));
return step();
});
}
function process_manifest(manifest) {
if (manifest.menu)
process_menu(manifest.menu);
if (manifest.tools)
process_menu(manifest.tools);
}
function process_menu(menu) {
for (var m in menu) {
if (menu[m].label) {
push({
msgid: menu[m].label,
locations: [ filename ]
});
}
}
}
/* Push an entry onto the list */
function push(entry) {
var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
var prev = entries[key];
if (prev) {
prev.locations = prev.locations.concat(entry.locations);
} else {
entries[key] = entry;
}
}
/* Escape a string for inclusion in po file */
function escape(string) {
var bs = string.split('\\').join('\\\\').split('"').join('\\"');
return bs.split("\n").map(function(line) {
return '"' + line + '"';
}).join("\n");
}
/* Finish by writing out the strings */
function finish() {
var result = [
'msgid ""',
'msgstr ""',
'"Project-Id-Version: PACKAGE_VERSION\\n"',
'"MIME-Version: 1.0\\n"',
'"Content-Type: text/plain; charset=UTF-8\\n"',
'"Content-Transfer-Encoding: 8bit\\n"',
'"X-Generator: Cockpit manifest2po\\n"',
'',
];
var msgid, entry;
for (msgid in entries) {
entry = entries[msgid];
result.push('#: ' + entry.locations.join(" "));
if (entry.msgctxt)
result.push('msgctxt ' + escape(entry.msgctxt));
result.push('msgid ' + escape(entry.msgid));
if (entry.msgid_plural) {
result.push('msgid_plural ' + escape(entry.msgid_plural));
result.push('msgstr[0] ""');
result.push('msgstr[1] ""');
} else {
result.push('msgstr ""');
}
result.push('');
}
var data = result.join('\n');
if (!opts.output) {
process.stdout.write(data);
process.exit(0);
} else {
fs.writeFile(opts.output, data, function(err) {
if (err)
fatal(err.message);
process.exit(0);
});
}
}

View file

@ -1,14 +0,0 @@
(function (root, data) {
var loaded, module;
/* Load into Cockpit locale */
if (typeof cockpit === 'object') {
cockpit.locale(data)
loaded = true;
}
if (!loaded)
root.po = data;
/* The syntax of this line is important by po2json */
}(this, {"":{"language":"en"}}));

View file

@ -1,127 +0,0 @@
#!/usr/bin/env node
function fatal(message, code) {
console.log((filename || "html2po") + ": " + message);
process.exit(code || 1);
}
function usage() {
console.log("usage: po2json [--module=template.js] input output");
process.exit(2);
}
var fs, po2json, Jed, stdio;
try {
fs = require('fs');
po2json = require('po2json');
Jed = require('jed');
stdio = require('stdio');
} catch(ex) {
fatal(ex.message, 127); /* missing looks for this */
}
var argi = 2;
var filename = null;
var opts = stdio.getopt({
module: { key: "m", args: 1, description: "Module template to include" },
output: { key: "o", args: 1, description: "Output file" },
});
if (opts.args.length != 1) {
usage();
}
parse();
function prepareHeader(header) {
var body, statement, plurals = header["plural-forms"], ret = null;
if (plurals) {
try {
/* Check that the plural forms isn't being sneaky since we build a function here */
Jed.PF.parse(plurals);
} catch(ex) {
fatal("bad plural forms: " + ex.message, 1);
}
/* A function for the front end */
statement = header["plural-forms"];
if (statement[statement.length - 1] != ';')
statement += ';';
ret = 'function(n) {\nvar nplurals, plural;\n' + statement + '\nreturn plural;\n}';
/* Added back in later */
delete header["plural-forms"];
}
/* We don't need to be transferring this */
delete header["project-id-version"];
delete header["report-msgid-bugs-to"];
delete header["pot-creation-date"];
delete header["po-revision-date"];
delete header["last-translator"];
delete header["language-team"];
delete header["mime-version"];
delete header["content-type"];
delete header["content-transfer-encoding"];
return ret;
}
/* Parse and process the po data */
function parse() {
filename = opts.args[0];
po2json.parseFile(opts.args[0], { "fuzzy": true }, function(err, jsonData) {
var plurals, pos;
if (err)
fatal(err.message);
var header = jsonData[""];
if (header)
plurals = prepareHeader(header);
var data = JSON.stringify(jsonData, null, 1);
/* We know the brace in is the location to insert our function */
if (plurals) {
pos = data.indexOf('{', 1);
data = data.substr(0, pos + 1) + "'plural-forms':" + String(plurals) + "," + data.substr(pos + 1);
}
if (data == JSON.stringify({}))
finish("");
else
wrap(data);
});
}
/* Wrap the data if desired */
function wrap(data) {
if (opts.module) {
filename = opts.module;
fs.readFile(opts.module, { encoding: "utf-8" }, function(err, template) {
if (err)
fatal(err.message);
data = template.replace('{"":{"language":"en"}}', data);
finish(data);
});
} else {
finish(data);
}
}
/* Write it out */
function finish(data) {
if (opts.output) {
fs.writeFile(opts.output, data, function(err) {
if (err)
fatal(err.message);
process.exit(0);
});
} else {
process.stdout.write(data);
process.exit(0);
}
}

View file

@ -19,29 +19,23 @@
import cockpit from 'cockpit';
import React from 'react';
import './app.scss';
import View from "./recordings.jsx";
const _ = cockpit.gettext;
export class Application extends React.Component {
constructor() {
super();
this.state = { 'hostname': _("Unknown") };
this.state = { hostname: _("Unknown") };
cockpit.file('/etc/hostname').read()
.done((content) => {
this.setState({ 'hostname': content.trim() });
cockpit.file('/etc/hostname').watch(content => {
this.setState({ hostname: content.trim() });
});
}
render() {
return (
<div className="container-fluid">
<h2>Starter Kit</h2>
<p>
{ cockpit.format(_("Running on $0"), this.state.hostname) }
</p>
</div>
<View />
);
}
}

View file

@ -1,3 +1,18 @@
@use "page.scss";
p {
font-weight: bold;
}
// Ensure UI fills the entire page (and does not run over)
.ct-page-fill {
height: 100% !important;
}
.config-container {
row-gap: var(--pf-global--spacer--sm);
> .pf-c-card {
min-width: 30rem;
}
}

662
src/config.jsx Normal file
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,17 +17,15 @@ along with this package; If not, see <http://www.gnu.org/licenses/>.
-->
<html lang="en">
<head>
<title translatable="yes">Cockpit Starter Kit</title>
<title translate>Cockpit Session Recording</title>
<meta charset="utf-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../base1/patternfly.css">
<link rel="stylesheet" href="index.css">
<script type="text/javascript" src="../base1/cockpit.js"></script>
<script type="text/javascript" src="../*/po.js"></script>
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="po.js"></script>
</head>
<body>

View file

@ -17,10 +17,14 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { Application } from './app.jsx';
import "cockpit-dark-theme";
import "patternfly/patternfly-5-cockpit.scss";
document.addEventListener("DOMContentLoaded", function () {
ReactDOM.render(React.createElement(Application, {}), document.getElementById('app'));
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Application } from './app.jsx';
import './app.scss';
document.addEventListener("DOMContentLoaded", () => {
createRoot(document.getElementById("app")).render(<Application />);
});

View file

@ -1,12 +1,26 @@
{
"version": "0.1",
"version": "163.x",
"name": "session-recording",
"requires": {
"cockpit": "137"
},
"tools": {
"menu": {
"index": {
"label": "Starter Kit"
"label": "Session Recording",
"order": 110,
"docs": [
{
"label": "Recording sessions",
"url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/recording_sessions/index"
}
],
"keywords": [
{
"matches": ["tlog", "sssd"]
}
]
}
}
}

84
src/player.css Normal file
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;
}
}

66
test/browser/browser.sh Executable file
View file

@ -0,0 +1,66 @@
#!/bin/sh
set -eux
export TEST_BROWSER=${TEST_BROWSER:-firefox}
export TESTS="$(realpath $(dirname "$0"))"
export SOURCE="$(realpath $TESTS/../..)"
export FILES="$(realpath $TESTS/../files)"
export LOGS="$(pwd)/logs"
mkdir -p "$LOGS"
chmod a+w "$LOGS"
# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2033020
dnf update -y pam || true
# install firefox (available everywhere in Fedora and RHEL)
# we don't need the H.264 codec, and it is sometimes not available (rhbz#2005760)
dnf install --disablerepo=fedora-cisco-openh264 -y --setopt=install_weak_deps=False firefox
# nodejs 10 is too old for current Cockpit test API
if grep -q platform:el8 /etc/os-release; then
dnf module switch-to -y nodejs:16
fi
# create user account for logging in
if ! id admin 2>/dev/null; then
useradd -c Administrator -G wheel admin
echo admin:foobar | chpasswd
fi
# set root's password
echo root:foobar | chpasswd
# avoid sudo lecture during tests
su -c 'echo foobar | sudo --stdin whoami' - admin
# create user account for running the test
if ! id runtest 2>/dev/null; then
useradd -c 'Test runner' runtest
# allow test to set up things on the machine
mkdir -p /root/.ssh
curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
fi
chown -R runtest "$SOURCE"
# disable core dumps, we rather investigate them upstream where test VMs are accessible
echo core > /proc/sys/kernel/core_pattern
## CSR specific setup ##
# install cockpit-packagekit and glibc-langpack-en for testAppMenu
dnf install -y cockpit-packagekit glibc-langpack-en
mkdir -p /var/log/journal/
cp $FILES/1.journal /var/log/journal/1.journal
cp $FILES/binary-rec.journal /var/log/journal/binary-rec.journal
systemctl enable --now cockpit.socket
# Run tests as unprivileged user
# once we drop support for RHEL 8, use this:
# runuser -u runtest --whitelist-environment=TEST_BROWSER,TEST_ALLOW_JOURNAL_MESSAGES,TEST_AUDIT_NO_SELINUX,SOURCE,LOGS $TESTS/run-test.sh
runuser -u runtest --preserve-environment env USER=runtest HOME=$(getent passwd runtest | cut -f6 -d:) $TESTS/run-test.sh
RC=$(cat $LOGS/exitcode)
exit ${RC:-1}

14
test/browser/main.fmf Normal file
View file

@ -0,0 +1,14 @@
require:
- cockpit-session-recording
- tlog
- cockpit-ws
- cockpit-system
- cockpit-packagekit
- bzip2
- git-core
- libvirt-python3
- make
- nodejs
- python3
test: ./browser.sh
duration: 30m

42
test/browser/run-test.sh Executable file
View file

@ -0,0 +1,42 @@
#!/bin/sh
set -eux
# tests need cockpit's bots/ libraries and test infrastructure
cd $SOURCE
git init
rm -f bots # common local case: existing bots symlink
make bots test/common
# support running from clean git tree
if [ ! -d node_modules/chrome-remote-interface ]; then
# copy package.json temporarily otherwise npm might try to install the dependencies from it
rm -f package-lock.json # otherwise the command below installs *everything*, argh
mv package.json .package.json
# only install a subset to save time/space
npm install chrome-remote-interface sizzle
mv .package.json package.json
fi
# disable detection of affected tests; testing takes too long as there is no parallelization
mv .git dot-git
. /etc/os-release
export TEST_OS="${ID}-${VERSION_ID/./-}"
if [ "${TEST_OS#centos-}" != "$TEST_OS" ]; then
TEST_OS="${TEST_OS}-stream"
fi
EXCLUDES=""
# make it easy to check in logs
echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}"
echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}"
RC=0
test/common/run-tests --nondestructive --machine 127.0.0.1:22 --browser 127.0.0.1:9090 $EXCLUDES || RC=$?
echo $RC > "$LOGS/exitcode"
cp --verbose Test* "$LOGS" || true
# deliver test result via exitcode file
exit 0

View file

@ -1,51 +1,412 @@
#!/usr/bin/python3
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
# Run this with --help to see available options for tracing and debugging
# See https://github.com/cockpit-project/cockpit/blob/master/test/common/testlib.py
# "class Browser" and "class MachineCase" for the available API.
import os
import sys
# import Cockpit's machinery for test VMs and its browser test API
TEST_DIR = os.path.dirname(__file__)
sys.path.append(os.path.join(TEST_DIR, "common"))
sys.path.append(os.path.join(os.path.dirname(TEST_DIR), "bots/machine"))
import testlib
import time
import json
import configparser
# Nondestructive tests all run in the same running VM. This allows them to run in Packit, Fedora, and RHEL dist-git gating
# They must not permanently change any file or configuration on the system in a way that influences other tests.
@testlib.nondestructive
class TestApplication(testlib.MachineCase):
def testBasic(self):
def _login(self, loc="/session-recording", wait="#app"):
self.login_and_go(loc)
b = self.browser
m = self.machine
b.wait_visible(wait)
self.allow_journal_messages('.*type=1400.*avc: denied .* comm="systemctl".*')
self.allow_journal_messages('.*invalid non-UTF8.*web_socket_connection_send.*')
self.allow_journal_messages('.*Locale charset.*ANSI.*')
self.allow_journal_messages('.*Assuming locale environment.*UTF-8.*')
return b, m
self.login_and_go("/starter-kit")
# verify expected heading
b.wait_present(".container-fluid h2")
b.wait_text(".container-fluid h2", "Starter Kit")
def _sel_rec(self, recording):
'''
rec1:
whoami
id
echo thisisatest123
sleep 16
echo thisisanothertest456
exit
# verify expected host name
hostname = m.execute("hostname").strip()
b.wait_present(".container-fluid p")
b.wait_text(".container-fluid p", "Running on " + hostname)
rec2:
echo "Extra Commands"
sudo systemctl daemon-reload
sudo ssh root@localhost
exit
# change language to German
b.switch_to_top()
b.click("#content-user-name")
b.click(".display-language-menu a")
b.wait_popup('display-language')
b.set_val("#display-language select", "de-de")
b.click("#display-language-select-button")
b.expect_load()
# HACK: work around language switching in Chrome not working in current session (Cockpit issue #8160)
b.reload(ignore_cache=True)
b.wait_present("#content")
# menu label (from manifest) should be translated
b.wait_text("#host-apps a[href='/starter-kit']", "Bausatz")
binaryrec:
mc
exit
'''
recordings = {'rec1': '0f25700a28c44b599869745e5fda8b0c-7106-121e79',
'rec2': '0f25700a28c44b599869745e5fda8b0c-7623-135541',
'binaryrec': '976e4ef1d66741848ed35f7600b94c5c-1a0f-c1ae'}
b.go("/starter-kit")
b.enter_page("/starter-kit")
# page label (from js) should be translated
b.wait_in_text(".container-fluid p", "Läuft auf")
page = recordings[recording]
if __name__ == '__main__':
self.browser.go(f"/session-recording#/{page}")
def _term_line(self, lineno):
return f".xterm-accessibility-tree div:nth-child({lineno})"
def testPlay(self):
b, _ = self._login()
self._sel_rec('rec1')
b.click("#player-play-pause")
b.wait_in_text(self._term_line(1), "localhost")
def testPlayBinary(self):
b, _ = self._login()
self._sel_rec('binaryrec')
b.click("#player-play-pause")
time.sleep(5)
b.wait_in_text(self._term_line(4), "exit")
def testFastforwardControls(self):
progress = ".pf-v5-c-progress__indicator"
b, _ = self._login()
self._sel_rec('rec1')
# fast forward
b.click("#player-fast-forward")
b.wait_in_text(self._term_line(12), "exit")
b.wait_attr(progress, "style", "width: 100%;")
# test restart playback
b.click("#player-restart")
b.click("#player-play-pause")
b.wait_text(self._term_line(7), "thisisatest123")
with b.wait_timeout(100):
b.wait_attr(progress, "style", "width: 100%;")
def testSpeedControls(self):
b, _ = self._login()
self._sel_rec('rec1')
# increase speed
b.wait_visible("#player-speed-up")
b.click("#player-speed-up")
b.wait_text("#player-speed", "x2")
b.click("#player-speed-up")
b.wait_text("#player-speed", "x4")
b.click("#player-speed-up")
b.wait_text("#player-speed", "x8")
b.click("#player-speed-up")
b.wait_text("#player-speed", "x16")
# decrease speed
b.click("#player-speed-down")
b.wait_text("#player-speed", "x8")
b.click("#player-speed-down")
b.wait_text("#player-speed", "x4")
b.click("#player-speed-down")
b.wait_text("#player-speed", "x2")
b.click("#player-speed-down")
b.click("#player-speed-down")
b.wait_text("#player-speed", "/2")
b.click("#player-speed-down")
b.wait_text("#player-speed", "/4")
b.click("#player-speed-down")
b.wait_text("#player-speed", "/8")
b.click("#player-speed-down")
b.wait_text("#player-speed", "/16")
# restore speed
b.click(".pf-v5-c-chip .pf-v5-c-button")
b.click("#player-speed-down")
b.wait_text("#player-speed", "/2")
def testZoomControls(self):
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
zoom_one_scale_sel = '.console-ct[style^="transform: scale(1.1)"]'
zoom_two_scale_sel = '.console-ct[style^="transform: scale(1.2)"]'
zoom_three_scale_sel = '.console-ct[style^="transform: scale(1.3)"]'
zoom_fit_to = (
'.console-ct[style*="translate(-50%, -50%)"]'
'[style*="top: 50%"]'
'[style*="left: 50%"]'
)
b, _ = self._login()
self._sel_rec('rec1')
# Wait for terminal with scale(1)
b.wait_visible(default_scale_sel)
# Zoom in x3
b.click("#player-zoom-in")
b.wait_visible(zoom_one_scale_sel)
b.click("#player-zoom-in")
b.wait_visible(zoom_two_scale_sel)
b.click("#player-zoom-in")
b.wait_visible(zoom_three_scale_sel)
# Zoom Out
b.click("#player-zoom-out")
b.wait_visible(zoom_two_scale_sel)
# Fit zoom to screen
b.click("#player-fit-to")
b.wait_visible(zoom_fit_to)
def testSkipFrame(self):
b, _ = self._login()
self._sel_rec('rec1')
b.wait_visible(self._term_line(1))
# loop until 3 valid frames have passed
while "localhost" not in b.text(self._term_line(1)):
b.click("#player-skip-frame")
b.wait_in_text(self._term_line(1), "localhost")
def testPlaybackPause(self):
b, _ = self._login()
self._sel_rec('rec1')
# Start and pause the player
b.click("#player-restart")
b.click("#player-play-pause")
b.click("#player-play-pause")
time.sleep(10)
# Make sure it didn't keep playing
b.wait_not_in_text(self._term_line(6), "thisisatest123")
# Test if it can start playing again
b.click("#player-play-pause")
def testSessionRecordingConf(self):
b, m = self._login()
b.click("#btn-config")
# TLOG config
conf_file_path = "/etc/tlog/"
conf_file = f"{conf_file_path}tlog-rec-session.conf"
save_file = "/tmp/tlog-rec-session.conf"
test_file = "/tmp/test-tlog-rec-session.conf"
# Save the existing config
b.click("#btn-save-tlog-conf")
m.download(conf_file, save_file)
# Change all of the fields
b.set_input_text("#shell", "/test/path/shell")
b.set_input_text("#notice", "Test Notice")
b.set_input_text("#latency", "1")
b.set_input_text("#payload", "2")
b.set_checked("#log_input", True)
b.set_checked("#log_output", False)
b.set_checked("#log_window", False)
b.set_input_text("#limit_rate", "3")
b.set_input_text("#limit_burst", "4")
b.set_val("#limit_action", "drop")
b.set_input_text("#file_path", "/test/path/file")
b.set_input_text("#syslog_facility", "testfac")
b.set_val("#syslog_priority", "info")
b.set_val("#journal_priority", "info")
b.set_checked("#journal_augment", False)
b.set_val("#writer", "file")
b.click("#btn-save-tlog-conf")
time.sleep(1)
m.download(conf_file, test_file)
# Revert to the previous config before testing to ensure test continuity
m.upload([save_file], conf_file_path)
# Check that the config reflects the changes
conf = json.load(open(test_file, "r"))
self.assertEqual(
json.dumps(conf),
json.dumps(
{
"shell": "/test/path/shell",
"notice": "Test Notice",
"latency": 1,
"payload": 2,
"log": {"input": True, "output": False, "window": False},
"limit": {"rate": 3, "burst": 4, "action": "drop"},
"file": {"path": "/test/path/file"},
"syslog": {"facility": "testfac", "priority": "info"},
"journal": {"priority": "info", "augment": False},
"writer": "file",
}
),
)
# SSSD config
conf_file_path = "/etc/sssd/conf.d/"
conf_file = f"{conf_file_path}sssd-session-recording.conf"
save_file = "/tmp/sssd-session-recording.conf"
test_none_file = "/tmp/test-none-sssd-session-recording.conf"
test_some_file = "/tmp/test-some-sssd-session-recording.conf"
test_all_file = "/tmp/test-all-sssd-session-recording.conf"
# Save the existing config
b.click("#btn-save-sssd-conf")
time.sleep(1)
m.download(conf_file, save_file)
# Download test with scope 'Some'
b.set_val("#scope", "some")
b.set_input_text("#users", "test users")
b.set_input_text("#groups", "test groups")
b.click("#btn-save-sssd-conf")
time.sleep(1)
m.download(conf_file, test_some_file)
# Download test with scope 'All'
b.set_val("#scope", "all")
b.set_input_text("#exclude_users", "testuser1")
b.set_input_text("#exclude_groups", "testgroup1")
b.click("#btn-save-sssd-conf")
time.sleep(1)
m.download(conf_file, test_all_file)
# Download test with scope 'None'
b.set_val("#scope", "none")
b.click("#btn-save-sssd-conf")
time.sleep(1)
m.download(conf_file, test_none_file)
# Revert to the previous config before testing to ensure test continuity
m.upload([save_file], conf_file_path)
# Check that the configs reflected the changes
conf = configparser.ConfigParser()
conf.read_file(open(test_some_file, "r"))
self.assertEqual(conf["session_recording"]["scope"], "some")
self.assertEqual(conf["session_recording"]["users"], "test users")
self.assertEqual(conf["session_recording"]["groups"], "test groups")
conf.read_file(open(test_all_file, "r"))
self.assertEqual(conf["session_recording"]["scope"], "all")
self.assertEqual(conf["session_recording"]["exclude_users"], "testuser1")
self.assertEqual(conf["session_recording"]["exclude_groups"], "testgroup1")
self.assertEqual(conf["domain/nssfiles"]["id_provider"], "proxy")
self.assertEqual(conf["domain/nssfiles"]["proxy_lib_name"], "files")
self.assertEqual(conf["domain/nssfiles"]["proxy_pam_target"], "sssd-shadowutils")
conf.read_file(open(test_none_file, "r"))
self.assertEqual(conf["session_recording"]["scope"], "none")
def testDisplayDrag(self):
b, _ = self._login()
self._sel_rec('rec1')
# start playback and pause in middle
b.click("#player-play-pause")
b.wait_in_text(self._term_line(1), "localhost")
b.click("#player-play-pause")
# zoom in so that the whole screen is no longer visible
b.click("#player-zoom-in")
b.click("#player-zoom-in")
# select and ensure drag'n'pan mode
b.click("#player-drag-pan")
# scroll and check for screen movement
b.mouse(".dragnpan", "mousedown", 200, 200)
b.mouse(".dragnpan", "mousemove", 10, 10)
self.assertNotEqual(b.attr(".dragnpan", "scrollTop"), 0)
self.assertNotEqual(b.attr(".dragnpan", "scrollLeft"), 0)
def testLogCorrelation(self):
b, m = self._login()
# make sure system is on expected timezone EST
m.execute("timedatectl set-timezone America/New_York")
# select the recording with the extra logs
self._sel_rec('rec2')
b.click("#btn-logs-view .pf-v5-c-expandable-section__toggle")
# fast forward until the end
while "exit" not in b.text(self._term_line(22)):
b.click("#player-skip-frame")
# check for extra log entries
b.wait_visible(".pf-v5-c-data-list:contains('authentication failure')")
def testZoomSpeedControls(self):
b, m = self._login()
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
self._sel_rec('rec1')
# set speed x16 and begin playing
for _ in range(4):
b.click("#player-speed-up")
b.wait_visible(default_scale_sel)
b.click("#player-play-pause")
# wait until sleeping and zoom in
b.wait_in_text(self._term_line(8), "sleep")
b.click("#player-zoom-in")
b.wait_not_present(default_scale_sel)
# zoom out while typing fast
b.wait_in_text(self._term_line(9), "localhost")
b.click("#player-zoom-out")
b.wait_not_present(default_scale_sel)
def _filter(self, inp, occ_dict):
m = self.machine
m.execute("timedatectl set-timezone America/New_York")
# ignore errors from half-entered timestamps due to searches occuring
# before `set_input_text` is complete
self.allow_journal_messages(".*timestamp.*")
# login and test inputs
b, _ = self._login()
time.sleep(5)
for occ in occ_dict:
for term in occ_dict[occ]:
# enter the search term and wait for the results to return
b.set_input_text(inp, term)
time.sleep(5)
self.assertEqual(b.text(".pf-v5-c-table").count("contractor"), occ)
def testSearch(self):
self._filter(
"#filter-search",
{
0: {
"this should return nothing",
"this should also return nothing",
"0123456789",
},
1: {
"extra commands",
"whoami",
"ssh",
"thisisatest123",
"thisisanothertest456",
},
2: {
"id",
"localhost",
"exit",
"actor",
"contractor",
"contractor1@localhost",
},
},
)
def testFilterUsername(self):
self._filter(
"#filter-username",
{
0: {"test", "contact", "contractor", "contractor11", "contractor4"},
2: {"contractor1"},
},
)
def testFilterSince(self):
self._filter(
"#filter-since",
{
0: {"2020-06-02", "2020-06-01 12:31:00"},
1: {"2020-06-01 12:17:01", "2020-06-01 12:30:50"},
2: {"2020-06-01", "2020-06-01 12:17:00"},
},
)
def testFilterUntil(self):
self._filter(
"#filter-until",
{
0: {"2020-06-01", "2020-06-01 12:16"},
1: {"2020-06-01 12:17", "2020-06-01 12:29"},
2: {"2020-06-02", "2020-06-01 12:31:00"},
},
)
def testAppMenu(self):
srrow = ".app-list .pf-v5-c-data-list__item-row:" \
"contains('Session Recording')"
srbut = "{} button:contains('Session Recording')" \
"".format(srrow)
b, _ = self._login("/apps", srrow)
self.allow_journal_messages(".*chromium-browser.appdata.xml.*",
".*xml.etree.ElementTree.ParseError:.*")
b.click(srbut)
b.enter_page("/session-recording")
b.wait_visible("#app")
if __name__ == "__main__":
testlib.test_main()

BIN
test/files/1.journal Normal file

Binary file not shown.

Binary file not shown.

1
test/reference-image Normal file
View file

@ -0,0 +1 @@
fedora-36

View file

@ -1,4 +1,14 @@
#! /bin/bash
#! /bin/sh
set -eu
# This is the expected entry point for Cockpit CI; will be called without
# arguments but with an appropriate $TEST_OS
# arguments but with an appropriate $TEST_OS, and optionally $TEST_SCENARIO
TEST_SCENARIO="${TEST_SCENARIO:-}"
[ "${TEST_SCENARIO}" = "${TEST_SCENARIO##firefox}" ] || export TEST_BROWSER=firefox
export RUN_TESTS_OPTIONS=--track-naughties
# linters are off by default for production builds, but we want to run them in CI
export LINT=1
make check

View file

@ -1,7 +1,7 @@
#!/bin/sh
# image-customize script to enable cockpit in test VMs
# The application RPM will be installed separately
set -eu
# image-customize script to prepare a bots VM for testing this application
# The application package will be installed separately
set -eux
# don't force https:// (self-signed cert)
printf "[WebService]\\nAllowUnencrypted=true\\n" > /etc/cockpit/cockpit.conf
@ -10,3 +10,6 @@ if type firewall-cmd >/dev/null 2>&1; then
firewall-cmd --add-service=cockpit --permanent
fi
systemctl enable cockpit.socket
# needed for testAppMenu
dnf install -y cockpit-packagekit

View file

@ -1,141 +0,0 @@
const path = require("path");
const copy = require("copy-webpack-plugin");
const extract = require("extract-text-webpack-plugin");
const fs = require("fs");
const webpack = require("webpack");
const CompressionPlugin = require("compression-webpack-plugin");
var externals = {
"cockpit": "cockpit",
};
/* These can be overridden, typically from the Makefile.am */
const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src";
const builddir = (process.env.SRCDIR || __dirname);
const distdir = builddir + path.sep + "dist";
const section = process.env.ONLYDIR || null;
const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules");
/* A standard nodejs and webpack pattern */
var production = process.env.NODE_ENV === 'production';
var info = {
entries: {
"index": [
"./index.es6"
]
},
files: [
"index.html",
"manifest.json",
],
};
var output = {
path: distdir,
filename: "[name].js",
sourceMapFilename: "[file].map",
};
/*
* Note that we're avoiding the use of path.join as webpack and nodejs
* want relative paths that start with ./ explicitly.
*
* In addition we mimic the VPATH style functionality of GNU Makefile
* where we first check builddir, and then srcdir.
*/
function vpath(/* ... */) {
var filename = Array.prototype.join.call(arguments, path.sep);
var expanded = builddir + path.sep + filename;
if (fs.existsSync(expanded))
return expanded;
expanded = srcdir + path.sep + filename;
return expanded;
}
/* Qualify all the paths in entries */
Object.keys(info.entries).forEach(function(key) {
if (section && key.indexOf(section) !== 0) {
delete info.entries[key];
return;
}
info.entries[key] = info.entries[key].map(function(value) {
if (value.indexOf("/") === -1)
return value;
else
return vpath(value);
});
});
/* Qualify all the paths in files listed */
var files = [];
info.files.forEach(function(value) {
if (!section || value.indexOf(section) === 0)
files.push({ from: vpath("src", value), to: value });
});
info.files = files;
var plugins = [
new copy(info.files),
new extract("[name].css")
];
/* Only minimize when in production mode */
if (production) {
/* Rename output files when minimizing */
output.filename = "[name].min.js";
plugins.unshift(new CompressionPlugin({
asset: "[path].gz[query]",
test: /\.(js|html)$/,
minRatio: 0.9,
deleteOriginalAssets: true
}));
}
module.exports = {
mode: production ? 'production' : 'development',
entry: info.entries,
externals: externals,
output: output,
devtool: "source-map",
module: {
rules: [
{
enforce: 'pre',
exclude: /node_modules/,
loader: 'eslint-loader',
test: /\.jsx$/
},
{
enforce: 'pre',
exclude: /node_modules/,
loader: 'eslint-loader',
test: /\.es6$/
},
{
exclude: /node_modules/,
loader: 'babel-loader',
test: /\.js$/
},
{
exclude: /node_modules/,
loader: 'babel-loader',
test: /\.jsx$/
},
{
exclude: /node_modules/,
loader: 'babel-loader',
test: /\.es6$/
},
{
exclude: /node_modules/,
loader: extract.extract('css-loader!sass-loader'),
test: /\.scss$/
}
]
},
plugins: plugins
}