diff --git a/.gitignore b/.gitignore
index 927248a..62c6bb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
*.rpm
node_modules/
dist/
+lib/
/*.spec
/.vagrant
package-lock.json
diff --git a/Makefile b/Makefile
index 77a9846..4d64e47 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,8 @@ VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS)
NODE_MODULES_TEST=package-lock.json
# one example file in dist/ from webpack to check if that already ran
WEBPACK_TEST=dist/index.html
+# one example file in src/lib to check if it was already checked out
+LIB_TEST=src/lib/cockpit-po-plugin.js
all: $(WEBPACK_TEST)
@@ -160,6 +162,12 @@ test/common:
git checkout --force FETCH_HEAD -- test/common
git reset test/common
+# checkout Cockpit's PF/React/build library; again this has no API stability guarantee, so check out a stable tag
+$(LIB_TEST):
+ git clone -b 256 --depth=1 https://github.com/cockpit-project/cockpit.git tmp/cockpit
+ mv tmp/cockpit/pkg/lib src/
+ rm -rf tmp/cockpit
+
$(NODE_MODULES_TEST): package.json
# 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
diff --git a/package.json b/package.json
index bd0b65b..4c467b3 100644
--- a/package.json
+++ b/package.json
@@ -51,10 +51,10 @@
"@patternfly/react-table": "^4.8.6",
"comment-json": "^1.1.3",
"core-js": "3.6.5",
+ "date-fns": "2.25.0",
"ini": "^1.3.5",
"jquery": "3.5.1",
"moment": "2.27.0",
- "mustache": "2.3.0",
"raw-loader": "^0.5.1",
"react": "16.13.1",
"react-dom": "16.13.1",
diff --git a/src/index.js b/src/index.js
index 85fa0c4..b718ab6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -17,7 +17,7 @@
* along with Cockpit; If not, see .
*/
-import "./lib/patternfly-4-cockpit.scss";
+import "./lib/patternfly/patternfly-4-cockpit.scss";
import "core-js/stable";
@@ -31,7 +31,7 @@ import { Application } from './app.jsx';
* out of the dist/index.js and since it will maintain the order of the imported CSS,
* the overrides will be correctly in the end of our stylesheet.
*/
-import "./lib/patternfly-4-overrides.scss";
+import "./lib/patternfly/patternfly-4-overrides.scss";
document.addEventListener("DOMContentLoaded", function () {
ReactDOM.render(React.createElement(Application, {}), document.getElementById('app'));
diff --git a/src/lib/_fonts.scss b/src/lib/_fonts.scss
deleted file mode 100644
index b3cb18a..0000000
--- a/src/lib/_fonts.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Keep in sync with https://github.com/cockpit-project/cockpit/tree/master/src/base1/_fonts.scss
- */
-
-@mixin printRedHatFont(
-$weightValue: 400,
-$weightName: "Regular",
-$familyName: "RedHatText",
-$style: "normal",
-$relative: true
-) {
- $filePath: "../../static/fonts" + "/" + $familyName + "-" + $weightName;
- @font-face {
- font-family: $familyName;
- src: url('#{$filePath}.woff2') format('woff2');
- font-style: #{$style};
- font-weight: $weightValue;
- text-rendering: optimizeLegibility;
- }
-}
-
-@include printRedHatFont(700, "Bold", $familyName: "RedHatDisplay");
-@include printRedHatFont(700, "BoldItalic", $style: "italic", $familyName: "RedHatDisplay");
-@include printRedHatFont(300, "Black", $familyName: "RedHatDisplay");
-@include printRedHatFont(300, "BlackItalic", $style: "italic", $familyName: "RedHatDisplay");
-@include printRedHatFont(300, "Italic", $style: "italic", $familyName: "RedHatDisplay");
-@include printRedHatFont(400, "Medium", $familyName: "RedHatDisplay");
-@include printRedHatFont(400, "MediumItalic", $style: "italic", $familyName: "RedHatDisplay");
-@include printRedHatFont(300, "Regular", $familyName: "RedHatDisplay");
-
-@include printRedHatFont(300, "Bold");
-@include printRedHatFont(300, "BoldItalic", $style: "italic");
-@include printRedHatFont(300, "Italic");
-@include printRedHatFont(700, "Medium");
-@include printRedHatFont(700, "MediumItalic", $style: "italic");
-@include printRedHatFont(400, "Regular");
diff --git a/src/lib/patternfly-4-cockpit.scss b/src/lib/patternfly-4-cockpit.scss
deleted file mode 100644
index 40da84e..0000000
--- a/src/lib/patternfly-4-cockpit.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Keep in sync with https://github.com/cockpit-project/cockpit/tree/master/src/base1/patternfly-4-cockpit.scss
- */
-
-/* Set fake font and icon path variables - we are going to indentify these through
- * string replacement and remove the relevant font-face declarations
- */
-$pf-global--font-path: 'patternfly-fonts-fake-path';
-$pf-global--fonticon-path: 'patternfly-icons-fake-path';
-$pf-global--disable-fontawesome: true !default; // Disable Font Awesome 5 Free
-@import '@patternfly/patternfly/patternfly-base.scss';
-
-/* Import our own fonts since the PF4 font-face rules are filtered out with patternfly.sed */
-@import "./fonts";
diff --git a/src/lib/patternfly-4-overrides.scss b/src/lib/patternfly-4-overrides.scss
deleted file mode 100644
index a02cb95..0000000
--- a/src/lib/patternfly-4-overrides.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Keep in sync with https://github.com/cockpit-project/cockpit/tree/master/pkg/lib/patternfly-4-overrides.scss
- */
-
-/*** PF4 overrides ***/
-
-/* WORKAROUND: Override word-break bug */
-/* See: https://github.com/patternfly/patternfly-next/issues/2325 */
-.pf-c-table td {
- word-break: normal;
- overflow-wrap: break-word;
-}
-
-/* WORKAROUND: Dropdown (PF4): Caret is not properly aligned bug */
-/* See: https://github.com/patternfly/patternfly/issues/2715 */
-/* Align the icons inside of all dropdown toggles. */
-/* Part 1 of 2 */
-.pf-c-dropdown__toggle-button {
- display: flex;
- align-items: center;
-}
-
-/* Make split button dropdowns the same height as their sibling. */
-/* Part 2 of 2 */
-.pf-m-split-button {
- align-items: stretch;
-}
-
-/* WORKAROUND: Navigation problems with Tertiary Nav widget on mobile */
-/* See: https://github.com/patternfly/patternfly-design/issues/840 */
-/* Helper mod to wrap pf-c-nav__tertiary */
-.ct-m-nav__tertiary-wrap {
- flex-wrap: wrap;
-
- .pf-c-nav__scroll-button {
- display: none;
- }
-}
-
-/* Helper mod to center pf-c-nav__tertiary when it wraps */
-.ct-m-nav__tertiary-center {
- justify-content: center;
-}
diff --git a/src/pkg/lib/cockpit-components-listing.jsx b/src/pkg/lib/cockpit-components-listing.jsx
deleted file mode 100644
index 19eea66..0000000
--- a/src/pkg/lib/cockpit-components-listing.jsx
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * This file is part of Cockpit.
- *
- * Copyright (C) 2016 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 .
- */
-
-import PropTypes from 'prop-types';
-import React from 'react';
-
-/* entry for an alert in the listing, can be expanded (with details) or standard
- * rowId optional: an identifier for the row which will be set as "data-row-id" attribute on the
- * columns list of columns to show in the header
- * columns to show, can be a string, react component or object with { name: 'name', 'header': false }
- * 'header' (or if simple string) defaults to false
- * in case 'header' is true,
is used for the entries, otherwise
- * tabRenderers optional: list of tab renderers for inline expansion, array of objects with
- * - name tab name (has to be unique in the entry, used as react key)
- * - renderer react component
- * - data render data passed to the tab renderer
- * - presence 'always', 'onlyActive', 'loadOnDemand', default: 'loadOnDemand'
- * - 'always' once a row is expanded, this tab is always rendered, but invisible if not active
- * - 'onlyActive' the tab is only rendered when active
- * - 'loadOnDemand' the tab is first rendered when it becomes active, then follows 'always' behavior
- * if tabRenderers isn't set, item can't be expanded inline
- * navigateToItem optional: callback triggered when a row is clicked, pattern suggests navigation
- * to view expanded item details, if not set, navigation isn't available
- * listingDetail optional: text rendered next to action buttons, similar style to the tab headers
- * listingActions optional: buttons that are presented as actions for the expanded item
- * selectChanged optional: callback will be used when the "selected" state changes
- * selected optional: true if the item is selected, can't be true if row has navigation or expansion
- * initiallyExpanded optional: the entry will be initially rendered as expanded, but then behaves normally
- * expandChanged optional: callback will be used if the row is either expanded or collapsed passing single `isExpanded` boolean argument
- */
-export class ListingRow extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- expanded: this.props.initiallyExpanded, // show expanded view if true, otherwise one line compact
- activeTab: this.props.initiallyActiveTab ? this.props.initiallyActiveTab : 0, // currently active tab in expanded mode, defaults to first tab
- loadedTabs: {}, // which tabs were already loaded - this is important for 'loadOnDemand' setting
- // contains tab indices
- selected: this.props.selected, // whether the current row is selected
- };
- this.handleNavigateClick = this.handleNavigateClick.bind(this);
- this.handleExpandClick = this.handleExpandClick.bind(this);
- this.handleSelectClick = this.handleSelectClick.bind(this);
- this.handleTabClick = this.handleTabClick.bind(this);
- }
-
- handleNavigateClick(e) {
- // only consider primary mouse button
- if (!e || e.button !== 0)
- return;
- this.props.navigateToItem();
- }
-
- handleExpandClick(e) {
- // only consider primary mouse button
- if (!e || e.button !== 0)
- return;
-
- const willBeExpanded = !this.state.expanded && this.props.tabRenderers.length > 0;
- this.setState({ expanded: willBeExpanded });
-
- const loadedTabs = {};
- // unload all tabs if not expanded
- if (willBeExpanded) {
- // see if we should preload some tabs
- let tabIdx;
- let tabPresence;
- for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) {
- if ('presence' in this.props.tabRenderers[tabIdx])
- tabPresence = this.props.tabRenderers[tabIdx].presence;
- else
- tabPresence = 'default';
- // the active tab is covered by separate logic
- if (tabPresence == 'always')
- loadedTabs[tabIdx] = true;
- }
- // ensure the active tab is loaded
- loadedTabs[this.state.activeTab] = true;
- }
-
- this.setState({ loadedTabs: loadedTabs });
-
- this.props.expandChanged && this.props.expandChanged(willBeExpanded);
-
- e.stopPropagation();
- e.preventDefault();
- }
-
- handleSelectClick(e) {
- // only consider primary mouse button
- if (!e || e.button !== 0)
- return;
-
- const selected = !this.state.selected;
- this.setState({ selected: selected });
-
- if (this.props.selectChanged)
- this.props.selectChanged(selected);
-
- e.stopPropagation();
- e.preventDefault();
- }
-
- handleTabClick(tabIdx, e) {
- // only consider primary mouse button
- if (!e || e.button !== 0)
- return;
- const prevTab = this.state.activeTab;
- let prevTabPresence = 'default';
- const loadedTabs = this.state.loadedTabs;
- if (prevTab !== tabIdx) {
- // see if we need to unload the previous tab
- if ('presence' in this.props.tabRenderers[prevTab])
- prevTabPresence = this.props.tabRenderers[prevTab].presence;
-
- if (prevTabPresence == 'onlyActive')
- delete loadedTabs[prevTab];
-
- // ensure the new tab is loaded and update state
- loadedTabs[tabIdx] = true;
- this.setState({ loadedTabs: loadedTabs, activeTab: tabIdx });
- }
- e.stopPropagation();
- e.preventDefault();
- }
-
- render() {
- const self = this;
- // only enable navigation if a function is provided and the row isn't expanded (prevent accidental navigation)
- const allowNavigate = !!this.props.navigateToItem && !this.state.expanded;
-
- const headerEntries = this.props.columns.map((itm, index) => {
- if (typeof itm === 'string' || typeof itm === 'number' || itm === null || itm === undefined || itm instanceof String || React.isValidElement(itm))
- return (
{itm}
);
- else if ('header' in itm && itm.header)
- return (
{itm.name}
);
- else if ('tight' in itm && itm.tight)
- return (
-
- );
- }
- }
-}
-
-ListingRow.defaultProps = {
- tabRenderers: [],
- navigateToItem: null,
-};
-
-ListingRow.propTypes = {
- rowId: PropTypes.string,
- columns: PropTypes.array.isRequired,
- tabRenderers: PropTypes.array,
- navigateToItem: PropTypes.func,
- listingDetail: PropTypes.node,
- listingActions: PropTypes.arrayOf(PropTypes.node),
- selectChanged: PropTypes.func,
- selected: PropTypes.bool,
- initiallyExpanded: PropTypes.bool,
- expandChanged: PropTypes.func,
- initiallyActiveTab: PropTypes.bool
-};
-/* Implements a PatternFly 'List View' pattern
- * https://www.patternfly.org/list-view/
- * Properties:
- * - title
- * - fullWidth optional: set width to 100% of parent, defaults to true
- * - emptyCaption header caption to show if list is empty, defaults to "No entries"
- * - columnTitles: array of column titles, as strings
- * - columnTitleClick: optional callback for clicking on column title (for sorting)
- * receives the column index as argument
- * - actions: additional listing-wide actions (displayed next to the list's title)
- */
-export const Listing = (props) => {
- const bodyClasses = ["listing", "listing-ct"];
- if (props.fullWidth)
- bodyClasses.push("listing-ct-wide");
- let headerClasses;
- let headerRow;
- let selectableRows;
- if (!props.children || props.children.length === 0) {
- headerClasses = "listing-ct-empty";
- headerRow =
{props.emptyCaption}
;
- } else if (props.columnTitles.length) {
- // check if any of the children are selectable
- selectableRows = false;
- props.children.forEach(function(r) {
- if (r.props.selected !== undefined)
- selectableRows = true;
- });
-
- if (selectableRows) {
- // now make sure that if one is set, it's available on all items
- props.children.forEach(function(r) {
- if (r.props.selected === undefined)
- r.props.selected = false;
- });
- }
-
- headerRow = (
-
- );
-};
-
-Listing.defaultProps = {
- title: '',
- fullWidth: true,
- columnTitles: [],
- actions: []
-};
-
-Listing.propTypes = {
- title: PropTypes.string,
- fullWidth: PropTypes.bool,
- emptyCaption: PropTypes.string.isRequired,
- columnTitles: PropTypes.arrayOf(
- PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.element,
- ])),
- columnTitleClick: PropTypes.func,
- actions: PropTypes.arrayOf(PropTypes.node)
-};
diff --git a/src/pkg/lib/console.css b/src/pkg/lib/console.css
deleted file mode 100644
index b8ee78b..0000000
--- a/src/pkg/lib/console.css
+++ /dev/null
@@ -1,59 +0,0 @@
-@import "term.css";
-
-/* Our terminal or logs */
-.console-ct {
- font-family: Menlo, Monaco, Consolas, monospace;
- margin-top: 0;
- margin-bottom: 0;
- font-size: 10px;
- text-align: center;
- line-height: normal;
-}
-
-@media (min-width: 568px) {
- .console-ct {
- font-size: 12px;
- }
-}
-
-.console-ct > pre {
- padding: 10px;
- text-align: left;
- display: block;
- font-family: inherit;
- font-size: inherit;
- width: 48em;
- height: 310px;
- overflow-y: scroll;
- white-space: pre-wrap;
- margin: 0 auto;
-}
-
-.console-ct > .terminal {
- color: #F0F0F0;
- text-align: left;
- outline: medium none;
- background-color: black;
- border: 1px solid black;
- padding: 10px;
-}
-
-.terminal .terminal-cursor {
- border: 1px solid #f0f0f0;
-}
-
-.terminal:focus .terminal-cursor {
- border: none;
- animation: blink 1s step-end infinite;
-}
-
-@keyframes blink {
- from {
- color: #000;
- background: #f0f0f0;
- }
- 50% {
- color: #f0f0f0;
- background: #000;
- }
-}
diff --git a/src/pkg/lib/journal.css b/src/pkg/lib/journal.css
deleted file mode 100644
index 8d023f9..0000000
--- a/src/pkg/lib/journal.css
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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 .
- */
-
-.cockpit-log-panel {
- border: 0;
-}
-
-.cockpit-log-panel .panel-heading {
- background-color: #333;
- border-color: #333;
- color: #fff;
- padding-left: 10px;
- padding-top: 5px;
- padding-bottom: 5px;
- height: auto;
-}
-
-.cockpit-log-panel .panel-body {
- padding: 0;
- border-bottom: 1px #ddd solid;
-}
-
-.cockpit-log-panel .panel-body .panel-heading {
- border-left: 1px #ddd solid;
- border-right: 1px #ddd solid;
- border-top: 0;
- border-bottom: 1px #ddd solid;
- background-color: #f5f5f5;
- font-weight: bold;
- padding-top: 2px;
- padding-bottom: 2px;
- width: auto;
- color: #333;
-}
-
-.cockpit-log-panel > .panel-heading {
- margin-top: 15px;
-}
-
-.cockpit-log-panel .cockpit-logline {
- border-left: 1px #ddd solid;
- border-right: 1px #ddd solid;
- background-color: #f5f5f5;
- padding-top: 2px;
- padding-bottom: 2px;
- padding-left: 10px;
-}
-
-.cockpit-logline {
- font-family: monospace;
- min-width: 310px;
- border-bottom: 1px solid #DDD;
- border-top: none;
-}
-
-.cockpit-logline > .row > div:first-child {
- padding-left: 20px;
-}
-
-.cockpit-log-panel .cockpit-logline:hover {
- background-color: #d4edfa;
-}
-
-.cockpit-log-panel > .cockpit-logline:hover {
- cursor: pointer;
-}
-
-.cockpit-logmsg-reboot {
- font-style: italic;
-}
-
-.cockpit-log-warning {
- display: inline-block;
- width: 20px;
- vertical-align: middle;
-}
-
-.cockpit-log-warning > i {
- color: black;
-}
-
-.cockpit-log-time {
- display: inline-block;
- width: 40px;
- vertical-align: middle;
-}
-
-.cockpit-log-service {
- width: 200px;
- margin-left: 10px;
-}
-
-.cockpit-log-service-container {
- display: inline-block;
- width: 200px;
- margin-left: 10px;
-}
-
-.cockpit-log-service-reduced {
- width: -moz-calc(100% - 70px);
- width: -webkit-calc(100% - 70px);
- width: calc(100% - 70px);
-}
-
-.cockpit-log-message {
- width: -moz-calc(100% - 300px);
- width: -webkit-calc(100% - 300px);
- width: calc(100% - 300px);
-}
-
-.cockpit-log-message, .cockpit-log-service, .cockpit-log-service-reduced {
- text-overflow: ellipsis;
- overflow: hidden;
- display: inline-block;
- white-space: nowrap;
- vertical-align: middle;
-}
-
diff --git a/src/pkg/lib/journal.js b/src/pkg/lib/journal.js
deleted file mode 100644
index e809a99..0000000
--- a/src/pkg/lib/journal.js
+++ /dev/null
@@ -1,576 +0,0 @@
-/*
- * 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 .
- */
-
-(function () {
- "use strict";
-
- var cockpit = require("cockpit");
- var Mustache = require("mustache");
- var day_header_template = require("raw-loader!journal_day_header.mustache");
- var line_template = require("raw-loader!journal_line.mustache");
- var reboot_template = require("raw-loader!journal_reboot.mustache");
-
- var _ = cockpit.gettext;
- var C_ = cockpit.gettext;
-
- var journal = {};
-
- /**
- * journalctl([match, ...], [options])
- * @match: any number of journal match strings
- * @options: an object containing further options
- *
- * Load and (by default) stream journal entries as
- * json objects. This function returns a jQuery deferred
- * object which delivers the various journal entries.
- *
- * The various @match strings are journalctl matches.
- * Zero, one or more can be specified. They must be in
- * string format, or arrays of strings.
- *
- * The optional @options object can contain the following:
- * * "host": the host to load journal from
- * * "count": number of entries to load and/or pre-stream.
- * Default is 10
- * * "follow": if set to false just load entries and don't
- * stream further journal data. Default is true.
- * * "directory": optional directory to load journal files
- * * "boot": when set only list entries from this specific
- * boot id, or if null then the current boot.
- * * "since": if specified list entries since the date/time
- * * "until": if specified list entries until the date/time
- * * "cursor": a cursor to start listing entries from
- * * "after": a cursor to start listing entries after
- *
- * Returns a jQuery deferred promise. You can call these
- * functions on the deferred to handle the responses. Note that
- * there are additional non-jQuery methods.
- *
- * .done(function(entries) { }): Called when done, @entries is
- * an array of all journal entries loaded. If .stream()
- * has been invoked then @entries will be empty.
- * .fail(funciton(ex) { }): called if the operation fails
- * .stream(function(entries) { }): called when we receive entries
- * entries. Called once per batch of journal @entries,
- * whether following or not.
- * .stop(): stop following or retrieving entries.
- */
-
- journal.journalctl = function journalctl(/* ... */) {
- var matches = [];
- var i;
- var arg;
- var options = { follow: true };
- for (i = 0; i < arguments.length; i++) {
- arg = arguments[i];
- if (typeof arg == "string") {
- matches.push(arg);
- } else if (typeof arg == "object") {
- if (arg instanceof Array) {
- matches.push.apply(matches, arg);
- } else {
- cockpit.extend(options, arg);
- break;
- }
- } else {
- console.warn("journal.journalctl called with invalid argument:", arg);
- }
- }
-
- if (options.count === undefined) {
- if (options.follow) options.count = 10;
- else options.count = null;
- }
-
- var cmd = ["journalctl", "--all", "-q", "--output=json"];
- if (!options.count) cmd.push("--no-tail");
- else cmd.push("--lines=" + options.count);
- if (options.directory) cmd.push("--directory=" + options.directory);
- if (options.boot) cmd.push("--boot=" + options.boot);
- else if (options.boot !== undefined) cmd.push("--boot");
- if (options.since) cmd.push("--since=" + options.since);
- if (options.until) cmd.push("--until=" + options.until);
- if (options.cursor) cmd.push("--cursor=" + options.cursor);
- if (options.after) cmd.push("--after=" + options.after);
- if (options.merge) cmd.push("-m");
- if (options.grep) cmd.push("--grep=" + options.grep);
-
- /* journalctl doesn't allow reverse and follow together */
- if (options.reverse) cmd.push("--reverse");
- else if (options.follow) cmd.push("--follow");
-
- cmd.push("--");
- cmd.push.apply(cmd, matches);
-
- var dfd = new cockpit.defer();
- var promise;
- var buffer = "";
- var entries = [];
- var streamers = [];
- var interval = null;
-
- function fire_streamers() {
- var ents, i;
- if (streamers.length && entries.length > 0) {
- ents = entries;
- entries = [];
- for (i = 0; i < streamers.length; i++)
- streamers[i].apply(promise, [ents]);
- } else {
- window.clearInterval(interval);
- interval = null;
- }
- }
-
- var proc = cockpit
- .spawn(cmd, {
- host: options.host,
- batch: 8192,
- latency: 300,
- superuser: "try",
- })
- .stream(function (data) {
- if (buffer) data = buffer + data;
- buffer = "";
-
- var lines = data.split("\n");
- var last = lines.length - 1;
- lines.forEach(function (line, i) {
- if (i == last) {
- buffer = line;
- } else if (line && line.indexOf("-- ") !== 0) {
- try {
- entries.push(JSON.parse(line));
- } catch (e) {
- console.warn(e, line);
- }
- }
- });
-
- if (streamers.length && interval === null)
- interval = window.setInterval(fire_streamers, 300);
- })
- .done(function () {
- fire_streamers();
- dfd.resolve(entries);
- })
- .fail(function (ex) {
- /* The journalctl command fails when no entries are matched
- * so we just ignore this status code */
- if (ex.problem == "cancelled" || ex.exit_status === 1) {
- fire_streamers();
- dfd.resolve(entries);
- } else {
- dfd.reject(ex);
- }
- })
- .always(function () {
- window.clearInterval(interval);
- });
-
- promise = dfd.promise();
- promise.stream = function stream(callback) {
- streamers.push(callback);
- return this;
- };
- promise.stop = function stop() {
- proc.close("cancelled");
- };
- return promise;
- };
-
- journal.printable = function printable(value) {
- if (value === undefined || value === null) return _("[no data]");
- else if (typeof value == "string") return value;
- else if (value.length !== undefined)
- return cockpit.format(_("[$0 bytes of binary data]"), value.length);
- else return _("[binary data]");
- };
-
- function output_funcs_for_box(box) {
- /* Dereference any jQuery object here */
- if (box.jquery) box = box[0];
-
- Mustache.parse(day_header_template);
- Mustache.parse(line_template);
- Mustache.parse(reboot_template);
-
- function render_line(ident, prio, message, count, time, entry) {
- var parts = {
- cursor: entry.__CURSOR,
- time: time,
- message: message,
- service: ident,
- };
- if (count > 1) parts.count = count;
- if (ident === "abrt-notification") {
- parts.problem = true;
- parts.service = entry.PROBLEM_BINARY;
- } else if (prio < 4) parts.warning = true;
- return Mustache.render(line_template, parts);
- }
-
- var reboot = _("Reboot");
- var reboot_line = Mustache.render(reboot_template, { message: reboot });
-
- function render_reboot_separator() {
- return reboot_line;
- }
-
- function render_day_header(day) {
- return Mustache.render(day_header_template, { day: day });
- }
-
- function parse_html(string) {
- var div = document.createElement("div");
- div.innerHTML = string.trim();
- return div.children[0];
- }
-
- return {
- render_line: render_line,
- render_day_header: render_day_header,
- render_reboot_separator: render_reboot_separator,
-
- append: function (elt) {
- if (typeof elt == "string") elt = parse_html(elt);
- box.appendChild(elt);
- },
- prepend: function (elt) {
- if (typeof elt == "string") elt = parse_html(elt);
- if (box.firstChild) box.insertBefore(elt, box.firstChild);
- else box.appendChild(elt);
- },
- remove_last: function () {
- if (box.lastChild) box.removeChild(box.lastChild);
- },
- remove_first: function () {
- if (box.firstChild) box.removeChild(box.firstChild);
- },
- };
- }
-
- var month_names = [
- C_("month-name", "January"),
- C_("month-name", "February"),
- C_("month-name", "March"),
- C_("month-name", "April"),
- C_("month-name", "May"),
- C_("month-name", "June"),
- C_("month-name", "July"),
- C_("month-name", "August"),
- C_("month-name", "September"),
- C_("month-name", "October"),
- C_("month-name", "November"),
- C_("month-name", "December"),
- ];
-
- /* Render the journal entries by passing suitable HTML strings back to
- the caller via the 'output_funcs'.
-
- Rendering is context aware. It will insert 'reboot' markers, for
- example, and collapse repeated lines. You can extend the output at
- the bottom and also at the top.
-
- A new renderer is created by calling 'journal.renderer' like
- so:
-
- var renderer = journal.renderer(funcs);
-
- You can feed new entries into the renderer by calling various
- methods on the returned object:
-
- - renderer.append(journal_entry)
- - renderer.append_flush()
- - renderer.prepend(journal_entry)
- - renderer.prepend_flush()
-
- A 'journal_entry' is one element of the result array returned by a
- call to 'Query' with the 'cockpit.journal_fields' as the fields to
- return.
-
- Calling 'append' will append the given entry to the end of the
- output, naturally, and 'prepend' will prepend it to the start.
-
- The output might lag behind what has been input via 'append' and
- 'prepend', and you need to call 'append_flush' and 'prepend_flush'
- respectively to ensure that the output is up-to-date. Flushing a
- renderer does not introduce discontinuities into the output. You
- can continue to feed entries into the renderer after flushing and
- repeated lines will be correctly collapsed across the flush, for
- example.
-
- The renderer will call methods of the 'output_funcs' object to
- produce the desired output:
-
- - output_funcs.append(rendered)
- - output_funcs.remove_last()
- - output_funcs.prepend(rendered)
- - output_funcs.remove_first()
-
- The 'rendered' argument is the return value of one of the rendering
- functions described below. The 'append' and 'prepend' methods
- should add this element to the output, naturally, and 'remove_last'
- and 'remove_first' should remove the indicated element.
-
- If you never call 'prepend' on the renderer, 'output_func.prepend'
- isn't called either. If you never call 'renderer.prepend' after
- 'renderer.prepend_flush', then 'output_func.remove_first' will
- never be called. The same guarantees exist for the 'append' family
- of functions.
-
- The actual rendering is also done by calling methods on
- 'output_funcs':
-
- - output_funcs.render_line(ident, prio, message, count, time, cursor)
- - output_funcs.render_day_header(day)
- - output_funcs.render_reboot_separator()
-
- */
-
- journal.renderer = function renderer(funcs_or_box) {
- var output_funcs;
- if (funcs_or_box.render_line) output_funcs = funcs_or_box;
- else output_funcs = output_funcs_for_box(funcs_or_box);
-
- function copy_object(o) {
- var c = {};
- for (var p in o) c[p] = o[p];
- return c;
- }
-
- // A 'entry' object describes a journal entry in formatted form.
- // It has fields 'bootid', 'ident', 'prio', 'message', 'time',
- // 'day', all of which are strings.
-
- function format_entry(journal_entry) {
- function pad(n) {
- var str = n.toFixed();
- if (str.length == 1) str = "0" + str;
- return str;
- }
-
- var d = new Date(journal_entry.__REALTIME_TIMESTAMP / 1000);
- return {
- cursor: journal_entry.__CURSOR,
- full: journal_entry,
- day:
- month_names[d.getMonth()] +
- " " +
- d.getDate().toFixed() +
- ", " +
- d.getFullYear().toFixed(),
- time: pad(d.getHours()) + ":" + pad(d.getMinutes()),
- bootid: journal_entry._BOOT_ID,
- ident: journal_entry.SYSLOG_IDENTIFIER || journal_entry._COMM,
- prio: journal_entry.PRIORITY,
- message: journal.printable(journal_entry.MESSAGE),
- };
- }
-
- function entry_is_equal(a, b) {
- return (
- a &&
- b &&
- a.day == b.day &&
- a.bootid == b.bootid &&
- a.ident == b.ident &&
- a.prio == b.prio &&
- a.message == b.message
- );
- }
-
- // A state object describes a line that should be eventually
- // output. It has an 'entry' field as per description above, and
- // also 'count', 'last_time', and 'first_time', which record
- // repeated entries. Additionally:
- //
- // line_present: When true, the line has been output already with
- // some preliminary data. It needs to be removed before
- // outputting more recent data.
- //
- // header_present: The day header has been output preliminarily
- // before the actual log lines. It needs to be removed before
- // prepending more lines. If both line_present and
- // header_present are true, then the header comes first in the
- // output, followed by the line.
-
- function render_state_line(state) {
- return output_funcs.render_line(
- state.entry.ident,
- state.entry.prio,
- state.entry.message,
- state.count,
- state.last_time,
- state.entry.full
- );
- }
-
- // We keep the state of the first and last journal lines,
- // respectively, in order to collapse repeated lines, and to
- // insert reboot markers and day headers.
- //
- // Normally, there are two state objects, but if only a single
- // line has been output so far, top_state and bottom_state point
- // to the same object.
-
- var top_state, bottom_state;
-
- top_state = bottom_state = {};
-
- function start_new_line() {
- // If we now have two lines, split the state
- if (top_state === bottom_state && top_state.entry) {
- top_state = copy_object(bottom_state);
- }
- }
-
- function top_output() {
- if (top_state.header_present) {
- output_funcs.remove_first();
- top_state.header_present = false;
- }
- if (top_state.line_present) {
- output_funcs.remove_first();
- top_state.line_present = false;
- }
- if (top_state.entry) {
- output_funcs.prepend(render_state_line(top_state));
- top_state.line_present = true;
- }
- }
-
- function prepend(journal_entry) {
- var entry = format_entry(journal_entry);
-
- if (entry_is_equal(top_state.entry, entry)) {
- top_state.count += 1;
- top_state.first_time = entry.time;
- } else {
- top_output();
-
- if (top_state.entry) {
- if (entry.bootid != top_state.entry.bootid)
- output_funcs.prepend(output_funcs.render_reboot_separator());
- if (entry.day != top_state.entry.day)
- output_funcs.prepend(
- output_funcs.render_day_header(top_state.entry.day)
- );
- }
-
- start_new_line();
- top_state.entry = entry;
- top_state.count = 1;
- top_state.first_time = top_state.last_time = entry.time;
- top_state.line_present = false;
- }
- }
-
- function prepend_flush() {
- top_output();
- if (top_state.entry) {
- output_funcs.prepend(
- output_funcs.render_day_header(top_state.entry.day)
- );
- top_state.header_present = true;
- }
- }
-
- function bottom_output() {
- if (bottom_state.line_present) {
- output_funcs.remove_last();
- bottom_state.line_present = false;
- }
- if (bottom_state.entry) {
- output_funcs.append(render_state_line(bottom_state));
- bottom_state.line_present = true;
- }
- }
-
- function append(journal_entry) {
- var entry = format_entry(journal_entry);
-
- if (entry_is_equal(bottom_state.entry, entry)) {
- bottom_state.count += 1;
- bottom_state.last_time = entry.time;
- } else {
- bottom_output();
-
- if (!bottom_state.entry || entry.day != bottom_state.entry.day) {
- output_funcs.append(output_funcs.render_day_header(entry.day));
- bottom_state.header_present = true;
- }
- if (bottom_state.entry && entry.bootid != bottom_state.entry.bootid)
- output_funcs.append(output_funcs.render_reboot_separator());
-
- start_new_line();
- bottom_state.entry = entry;
- bottom_state.count = 1;
- bottom_state.first_time = bottom_state.last_time = entry.time;
- bottom_state.line_present = false;
- }
- }
-
- function append_flush() {
- bottom_output();
- }
-
- return {
- prepend: prepend,
- prepend_flush: prepend_flush,
- append: append,
- append_flush: append_flush,
- };
- };
-
- journal.logbox = function logbox(match, max_entries) {
- var entries = [];
- var box = document.createElement("div");
-
- function render() {
- var renderer = journal.renderer(box);
- while (box.firstChild) box.removeChild(box.firstChild);
- for (var i = 0; i < entries.length; i++) {
- renderer.prepend(entries[i]);
- }
- renderer.prepend_flush();
- if (entries.length > 0) box.removeAttribute("hidden");
- else box.setAttribute("hidden", "hidden");
- }
-
- render();
-
- var promise = journal
- .journalctl(match, { count: max_entries })
- .stream(function (tail) {
- entries = entries.concat(tail);
- if (entries.length > max_entries) entries = entries.slice(-max_entries);
- render();
- })
- .fail(function (error) {
- box.appendChild(document.createTextNode(error.message));
- box.removeAttribute("hidden");
- });
-
- /* Both a DOM element and a promise */
- return promise.promise(box);
- };
-
- module.exports = journal;
-})();
diff --git a/src/pkg/lib/journal_day_header.mustache b/src/pkg/lib/journal_day_header.mustache
deleted file mode 100644
index 708ae04..0000000
--- a/src/pkg/lib/journal_day_header.mustache
+++ /dev/null
@@ -1 +0,0 @@
-