List certificates

This commit is contained in:
Simon Kobyda 2020-01-07 16:53:13 +01:00
parent a3a94f127f
commit e242d229e4
5 changed files with 666 additions and 10 deletions

403
lib/form-layout.less Normal file
View file

@ -0,0 +1,403 @@
/* Form layout */
@import "./variables.less";
:root {
// CSS variable to define the number of (label + control) columns.
// It dynamically changes on narrow pages (see media query below).
--ct-form-columns: 2;
}
// Cockpit Form Layout: Automatically have Cockpit display your form in
// an optimal layout.
//
// By default, all labels are aligned and sized properly and form elements
// stretch to take up the remaining space.
//
//
// There are additional classes and attributes you can add to each
// control directly under `ct-form`:
//
// `ct-form-split`: The grid can be split on a `form-control`
// level by adding a this class. If you want two elements next to each
// other, both should have this class. Widths are equal by default.
// See ct-form-minmax & ct-form-maxmin for alternate sizing.
//
// `ct-form-relax`: Form elements normally stretch to take up the
// full space. You can relax their width by adding this class to the
// control. Inputs with a size attribute are auto-relaxed and do not
// need this class.
//
// `ct-form-stretch`: If a control has a width specified
// elsewhere, you can force it to stretch. This is mainly useful when
// using <div role="group"> to group elements.
//
// `ct-form-full`: Force a widget to be the full width of the form,
// invading the label space.
//
// role="group": When there are two related elements, such as a text
// input and a dropdown, you can group them together using this HTML
// attribute. It's similar in purpose to a <fieldset>, but works for
// layouts in Chrome (unlike fieldset). This can be attached to any
// container element, but will most likely be used with <div>. The role
// adds semantic meaning to the element for screen readers, and we key
// the CSS off of the role.
//
// `ct-form-box`: Visual styling for encapsulating a block of sub-options.
// Creates a gray box around elements.
//
// <hr>: While this is an element, it has a special meaning and is used
// to add some vertical spacing to a form.
//
//
// Alternate grid sizing:
// You can override division of space for controls by adding a class
// at grid level (.ct-form) to adjust size for "split" widgets:
// `ct-form-maxmin: First widget is wide; second is small.
// `ct-form-minmax`: First widget is small; second is wide.
//
//
// Most of the time, you can simply ignore all the optional classes (and
// attribute and hr element) and simply wrap your labels & controls in
// a <form class="ct-form"> and layout magic happens.
.ct-form {
// Locally redefine padding to Bootstrap values for this LESS block
@padding-large-vertical: 1rem;
@padding-small-vertical: 0.5rem;
@padding-small-horizontal: 1.5rem;
// Bootstrap & PatternFly use a 1px border around widgets
@border-width: 1px;
@widget-height: 2.25rem; // (36px for PF4 widgets)
align-self: start; // Don't vertically fill content by default
display: grid;
grid-gap: @padding-small-vertical @padding-small-horizontal;
// Repeat a label that is a minimum of 4em and its control that
// fills the remaining space by a CSS variable (default: 2)
grid-template-columns: repeat(var(--ct-form-columns), max-content 1fr);
justify-items: stretch;
align-content: baseline;
// All <label> elements describing form elements in PatternFly are
// supposed to have a `control-label` class.
// These precede control elements.
> .control-label {
text-align: right;
padding: 0;
margin: 0;
}
> :not(hr):not(p) {
line-height: 2.25rem;
}
> p {
margin: 0;
}
// Put all control elements to the right of the labels,
// stretching to the rightmost column
> :not(.control-label):not(hr):not(.ct-form-split):not(.ct-form-full) {
grid-column: ~"2 / -1";
}
// Auto-stretch elements to the grid (except when relaxed)
> :not(.ct-form-relax):not(.spinner) {
width: auto;
}
// Horizontal rules directly under a form-layout container serve to
// add some vertical space in forms. This is useful for visually
// grouping similar elements with whitespace.
//
// It's not the same as actually grouping elements (which can be done
// in the usual ways as well as adding a role="group".
> hr {
border: none;
// LESS needs this to be escaped with ~"". You'll see it below too.
// CSS wants the string to be 1 / -1 without escaping.
grid-column: ~"1 / -1";
height: 0.5rem;
// Reset padding to ensure all browsers treat this the same
margin: 0;
padding: 0;
}
// Auto-relax inputs with size
> input[size],
> .ct-validation-wrapper > input[size] {
justify-self: start;
}
> .ct-validation-wrapper {
display: flex;
flex-direction: column;
}
// Hack to allow number inputs to be sized on WebKit-based browsers
input[type=number] {
-webkit-appearance: textarea;
}
// Special considerations for widgets (and widget-like elements)
// This is a LESS mixin that will not be in the compiled CSS.
.widget-rules() {
> input,
> textarea,
> select,
> .bootstrap-select,
> .ct-select,
> .dropdown,
> .combobox-container,
> fieldset,
> [role=group],
> [data-field],
> .form-group,
> .btn-group,
> label.checkbox,
> label.radio,
> .checkbox-inline,
> .radio-inline {
line-height: var(--pf-global--LineHeight--md);
}
}
&, > .ct-validation-wrapper {
.widget-rules();
}
// Some elements need special width considerations
// as PatternFly normally fixes the width
> :not(.ct-form-relax):not(.spinner) {
width: auto !important;
}
// Elements with role="group" are used to group elements —
// fieldset was going to be used, but Chrome doesn't allow
// grid or flex placement for fieldsets (yet).
//
// Adding a group role is the same thing accessibilty-wise
// and lets us target all browsers properly.
//
// You can use this like:
// <div role="group">
//
// And non-div elements are also supported.
> [role=group],
> .ct-validation-wrapper > [role=group] {
align-self: start;
align-content: center;
display: grid;
grid-gap: @padding-small-vertical;
min-height: 2.25rem;
justify-content: start;
// Only support 3 splits for now (can change to 3 later, if needed)
grid-template-columns: repeat(3, auto);
&.ct-form-vertical {
> :not(.ct-form-split) {
// Stretch across the grid (unless it's a split)
grid-column: ~"1 / -1";
}
}
> .checkbox,
> .radio {
// Spacing is handled by grid, not margin
margin: 0;
&:first-child {
margin-top: 0.5rem;
}
}
}
> [role=group],
> .ct-validation-wrapper > [role=group],
> .ct-validation-wrapper > [data-field] {
// Allow dropdowns to expand as needed
&:not(.ct-form-relax) {
> .dropdown {
width: auto !important;
}
// <select>s need to be coaxed to be 100%
> .ct-select {
width: 100%;
}
}
}
// Vertically align checkboxes and radios properly using flex
label.checkbox,
label.radio,
.checkbox > label,
.radio > label,
.checkbox-inline,
.radio-inline {
display: inline-flex;
padding-left: 0;
padding-right: @padding-small-horizontal;
align-items: center;
> input[type="checkbox"],
> input[type="radio"] {
margin: 0 0.5em 0 0;
position: static;
}
}
// Remove vertical spacing for fieldsets,
// as this is handled by the grid gap
fieldset {
> .checkbox,
> .radio {
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
}
// List groups override the grid gap, so we're adding it manually
.list-group {
margin-bottom: @padding-small-vertical;
}
// Relax split elements to only take up one column
> .ct-form-split {
grid-column: ~"auto / auto";
}
// Stretch to full width
> .ct-form-full {
grid-column: ~"1 / -1";
}
// Move warnings, errors, info, etc. up a bit to associate with previous field
.bump-up() {
position: relative;
margin-top: -0.5rem;
}
> .has-success,
> .has-warning,
> .has-error {
&:not(.form-group):not(fieldset):not([role=group]) {
//.bump-up();
}
}
> .help-block {
.bump-up();
}
.help-block {
--help-line-height: calc(var(--pf-global--LineHeight--md) * 1rem);
line-height: var(--help-line-height);
&:empty {
display: none;
}
.spinner {
position: relative;
// (baseline - height - border) / 2
top: calc((var(--help-line-height) - 16px - 2px) / 2);
}
}
.ct-form-box {
background: var(--color-gray-1);
border-width: 1px;
border-style: solid;
border-color: var(--color-gray-5);
padding: 0.5rem;
padding-top: 1rem;
width: 100%;
}
}
// Force a form element to stretch. Add as a class to `form-control`.
.ct-form-stretch {
justify-content: stretch !important;
}
// Instruct a `form-control` to not stretch.
.ct-form-relax {
justify-self: start;
}
// Reset .ct-form-split for small dialogs, as they don't have
// much width. This allows for using the same HTML layout in both
// narrow and normal dialogs.
.modal-dialog.modal-sm .ct-form > .ct-form-split {
grid-column: ~"2 / -1";
}
@media (max-width: 640px) {
// When inside of lists or modals & the page isn't wide enough,
// collapse (label + control) columns down to 1, to force splits on
// their own lines
.listing-ct-body,
.modal {
--ct-form-columns: 1;
}
}
// Alternate layout, for a split, used at ct-form grid-level:
// First form widget is as small as possible;
// Second takes up the rest of the space
.ct-form-minmax {
grid-template-columns: max-content min-content max-content 1fr;
}
// Alternate layout, for a split, used at ct-form grid-level:
// First form widget takes up as much space as it can;
// Second form widget is as small as possible
.ct-form-maxmin {
grid-template-columns: max-content 1fr max-content min-content;
}
@media (max-width: @screen-xs) {
// When inside of lists or modals & the page is *very* narrow,
// collapse the grid further, so labels are above controls
//
// Note: Padding variables below are outside the local scope of the
// .ct-form block, so they default to the global PatternFly
// values.
.listing-ct-body,
.modal {
.ct-form {
// Completely deconstruct the grid layout
grid-template-columns: initial;
> * {
// Don't restrict grid placement
grid-column: auto;
max-width: 100%;
}
// As control labels fill the row, left align and remove padding
> .control-label {
padding: 0;
text-align: left;
// Everything but the first label should have space to breathe
&:not(:first-child) {
margin: @padding-large-vertical 0 0;
}
}
// Reduce vertical height spacing between groups of elements
> hr {
height: @padding-large-vertical * 2;
}
}
}
}

23
lib/variables.less Normal file
View file

@ -0,0 +1,23 @@
@import (less) "../node_modules/bootstrap-less/bootstrap/variables.less";
@import (less) "../node_modules/patternfly/dist/less/variables.less";
@metadata-color: var(--color-subtle-copy);
@listing-ct-hover: var(--color-ct-list-hover-bg);
@listing-ct-hover-icon: var(--color-ct-list-hover-icon);
@listing-ct-active: var(--color-ct-list-active-bg);
@listing-ct-padding: 0.5rem;
@listing-ct-spacing: 1rem;
@listing-ct-open: #f5f5f5;
@listing-ct-open-width: 3px;
@listing-ct-metadata: @metadata-color;
@listing-ct-warning-color: var(--color-ct-light-red-1);
@listing-ct-border: var(--color-light-gray);
@listing-ct-border-light: var(--color-gray-2);
@listing-ct-border-maybe: var(--color-light-gray-3);
@screen-lg-max: (@screen-xlg-min - 1);
@screen-xlg-min: 1600px;
@screen-xs: 480px;
@screen-xs-min: @screen-xs;
@screen-xxs-max: (@screen-xs-min - 1);

View file

@ -1,7 +1,7 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 Red Hat, Inc.
* Copyright (C) 2020 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
@ -17,9 +17,13 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from 'cockpit';
import React from 'react';
import './app.scss';
import cockpit from "cockpit";
import React from "react";
import "./app.scss";
// import { Accordion, AccordionItem, AccordionContent, AccordionToggle } from "@patternfly/react-core";
import CertificateList from "./certificateList.jsx";
const _ = cockpit.gettext;
@ -28,20 +32,16 @@ export class Application extends React.Component {
super();
this.state = { hostname: _("Unknown") };
cockpit.file('/etc/hostname').watch(content => {
cockpit.file("/etc/hostname").watch(content => {
this.setState({ hostname: content.trim() });
});
}
render() {
const path = getRequests();
return (
<div className="container-fluid">
<h2>Certificates</h2>
<p>
{ cockpit.format(_("Running on $0"), path) }
</p>
<CertificateList />
</div>
);
}

193
src/certificateList.jsx Normal file
View file

@ -0,0 +1,193 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2020 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
// import cockpit from "cockpit";
import React from "react";
import moment from "moment";
import { Accordion, AccordionItem, AccordionContent, AccordionToggle, Flex, FlexItem, FlexModifiers } from "@patternfly/react-core";
import '../lib/form-layout.less';
import { getRequests, getRequest } from "./dbus.js";
const _ = cockpit.gettext;
function prettyTime(unixTime) {
moment.locale(cockpit.language, {
longDateFormat : {
LT: "hh:mm:ss",
L: "DD/MM/YYYY",
}
});
const yesterday = _("Yesterday");
const today = _("Today");
moment.locale(cockpit.language, {
calendar : {
lastDay : `[${yesterday}] LT`,
sameDay : `[${today}] LT`,
sameElse : "L LT"
}
});
return moment(Number(unixTime) * 1000).calendar();
}
class CertificateList extends React.Component {
constructor() {
super();
this.state = {
certs: [],
expanded: [],
};
this.toggle = this.toggle.bind(this);
this.onValueChanged = this.onValueChanged.bind(this);
getRequests()
.fail(error => {
console.log(JSON.stringify(error)); // TODO better error handling
})
.then(paths => {
paths[0].forEach(p => {
getRequest(p)
.fail(error => {
console.log(JSON.stringify(error)); // TODO better error handling
})
.then(ret => {
const certs = [...this.state.certs, ret[0]];
this.onValueChanged("certs", certs);
});
});
});
}
toggle(certId) {
const expanded = [...this.state.expanded];
const certIndex = expanded.findIndex(e => e === certId);
if (certIndex < 0)
expanded.push(certId);
else
expanded.splice(certIndex, 1);
this.setState({ expanded });
}
onValueChanged(key, value) {
this.setState({ [key]: value });
}
render() {
const certs = this.state.certs;
console.log(certs);
const items = certs.map(cert => (
<AccordionItem key={cert.nickname.v}>
<AccordionToggle
onClick={() => this.toggle(cert.nickname.v)}
isExpanded={this.state.expanded.includes(cert.nickname.v)}
id={cert.nickname.v + "toggle"}
>
{cert["cert-nickname"].v}
</AccordionToggle>
<AccordionContent
id={cert.nickname.v + "content"}
isHidden={!this.state.expanded.includes(cert.nickname.v)}
isFixed
>
<div className="overview-tab-grid">
<label className='control-label label-title'> {_("General")} </label>
<span />
<Flex breakpointMods={[{modifier: FlexModifiers["justify-content-space-between"]}]}>
<Flex breakpointMods={[{modifier: FlexModifiers["column", "flex-1"]}]}>
<div className="ct-form">
<label className='control-label label-title'>{_("Status")}</label>
<div>{cert.status.v}</div>
<label className='control-label label-title'>{_("Auto-renewal")}</label>
<div>{cert.autorenew.v ? _("Yes") : _("No")}</div>
<label className='control-label label-title'>{_("Stuck")}</label>
<div>{cert.stuck.v ? _("Yes") : _("No")}</div>
</div>
</Flex>
<Flex breakpointMods={[{modifier: FlexModifiers["column", "flex-1"]}]}>
<div className="ct-form">
<label className='control-label label-title'>{_("Not valid after")}</label>
<div>{prettyTime(cert["not-valid-after"].v)}</div>
<label className='control-label label-title'>{_("Not valid before")}</label>
<div>{prettyTime(cert["not-valid-before"].v)}</div>
</div>
</Flex>
</Flex>
<label className='control-label label-title'> {_("Key")} </label>
<span />
<Flex breakpointMods={[{modifier: FlexModifiers["justify-content-space-between"]}]}>
<Flex breakpointMods={[{modifier: FlexModifiers["column", "flex-1"]}]}>
<div className="ct-form">
<label className='control-label label-title'>{_("Nickname")}</label>
<div>{cert["key-nickname"].v}</div>
<label className='control-label label-title'>{_("Type")}</label>
<div>{cert["key-type"].v}</div>
<label className='control-label label-title'>{_("Token")}</label>
<div>{cert["key-token"].v}</div>
</div>
</Flex>
<Flex breakpointMods={[{modifier: FlexModifiers["column", "flex-1"]}]}>
<div className="ct-form">
<label className='control-label label-title'>{_("Location")}</label>
<div>{cert["key-database"].v}</div>
<label className='control-label label-title'>{_("Storage")}</label>
<div>{cert["key-storage"].v}</div>
</div>
</Flex>
</Flex>
<label className='control-label label-title'> {_("Cert")} </label>
<span />
<Flex breakpointMods={[{modifier: FlexModifiers["justify-content-space-between"]}]}>
<Flex breakpointMods={[{modifier: FlexModifiers["column", "flex-1"]}]}>
<div className="ct-form">
<label className='control-label label-title'>{_("Nickname")}</label>
<div>{cert["cert-nickname"].v}</div>
<label className='control-label label-title'>{_("Token")}</label>
<div>{cert["cert-token"].v}</div>
</div>
</Flex>
<Flex breakpointMods={[{modifier: FlexModifiers["column", "flex-1"]}]}>
<div className="ct-form">
<label className='control-label label-title'>{_("Location")}</label>
<div>{cert["cert-database"].v}</div>
<label className='control-label label-title'>{_("Storage")}</label>
<div>{cert["cert-storage"].v}</div>
</div>
</Flex>
</Flex>
</div>
</AccordionContent>
</AccordionItem>
));
return (
<Accordion asDefinitionList={false}>
{items}
</Accordion>
);
}
}
export default CertificateList;

37
src/dbus.js Normal file
View file

@ -0,0 +1,37 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2020 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from "cockpit";
function dbusCall(objectPath, iface, method, args) {
const clientCertmonger = cockpit.dbus("org.fedorahosted.certmonger",
{ superuser: "try" });
return clientCertmonger.call(objectPath, iface, method, args);
}
export function getRequest(path) {
return dbusCall(path, "org.freedesktop.DBus.Properties", "GetAll",
["org.fedorahosted.certmonger.request"]);
}
export function getRequests() {
return dbusCall("/org/fedorahosted/certmonger", "org.fedorahosted.certmonger",
"get_requests", []);
}