* Copyright (C) 2016 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import React, { useState } from 'react';
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle/index.js';
import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
import { BarsIcon, EllipsisVIcon } from '@patternfly/react-icons';
import cockpit from "cockpit";
import * as utils from "./utils.js";
import client from "./client.js";
import { dialog_open } from "./dialog.jsx";
const _ = cockpit.gettext;
* a privileged action.
*
* It can be disabled and will show a tooltip then. It will
* automatically disappear when the logged in user doesn't
* have permission.
*
* Properties:
*
* - excuse: If set, the button/link is disabled and will show the
* excuse in a tooltip.
*/
class StorageControl extends React.Component {
render() {
const excuse = this.props.excuse;
if (!client.superuser.allowed)
return <div />;
if (excuse) {
return (
<Tooltip id="tip-storage" content={excuse}
position={this.props.excuse_placement || TooltipPosition.top}>
<span>
{ this.props.content(excuse) }
</span>
</Tooltip>
);
} else {
return this.props.content();
}
}
}
function checked(callback, setSpinning, excuse) {
return function (event) {
if (!event)
return;
if (event.type === 'click' && event.button !== 0)
return;
if (event.type === 'KeyDown' && event.key !== "Enter")
return;
event.stopPropagation();
if (excuse) {
dialog_open({
Title: _("Sorry"),
Body: excuse
});
return;
}
const promise = client.run(callback);
if (promise) {
if (setSpinning)
setSpinning(true);
promise.finally(() => {
if (setSpinning)
setSpinning(false);
});
promise.catch(function (error) {
console.warn(error.toString());
dialog_open({
Title: _("Error"),
Body: error.toString()
});
});
}
};
}
export const StorageButton = ({ id, kind, excuse, onClick, children, ariaLabel, spinner }) => {
const [spinning, setSpinning] = useState(false);
return <StorageControl excuse={excuse}
content={excuse => (
<Button id={id}
aria-label={ariaLabel}
onClick={checked(onClick, setSpinning)}
variant={kind || "secondary"}
isDisabled={!!excuse || (spinner && spinning)}
isLoading={spinner ? spinning : undefined}>
{children}
</Button>
)} />;
};
export const StorageLink = ({ id, excuse, onClick, children }) => (
<StorageControl excuse={excuse}
content={excuse => (
<Button onClick={checked(onClick)}
variant="link"
isInline
isDisabled={!!excuse}>
{children}
</Button>
)} />
);
export class StorageOnOff extends React.Component {
constructor() {
super();
this.state = { promise: null };
}
render() {
const self = this;
function onChange(_event, val) {
const promise = self.props.onChange(val);
if (promise) {
promise.catch(error => {
dialog_open({
Title: _("Error"),
Body: error.toString()
});
})
.finally(() => { self.setState({ promise: null }) });
}
self.setState({ promise, promise_goal_state: val });
}
return (
<StorageControl excuse={this.props.excuse}
content={(excuse) => (
<Switch isChecked={this.state.promise
? this.state.promise_goal_state
: this.props.state}
aria-label={this.props['aria-label']}
isDisabled={!!(excuse || this.state.promise)}
onChange={onChange} />
)} />
);
}
}
* bytes in use. If the ratio is above props.critical, the bar will be
* in a dangerous color.
*/
export const StorageUsageBar = ({ stats, critical, block, offset, total, short }) => {
if (!stats)
return null;
const fraction = stats[0] / stats[1];
const off_fraction = offset / stats[1];
const total_fraction = total / stats[1];
const labelText = utils.format_fsys_usage(stats[0], stats[1]);
return (
<div>
<span className="usage-text pf-v6-u-text-nowrap">
{labelText}
</span>
<div className={"usage-bar" + (fraction > critical ? " usage-bar-danger" : "") + (short ? " usage-bar-short" : "")}
role="progressbar"
aria-valuemin={0} aria-valuemax={stats[1]} aria-valuenow={stats[0]}
aria-label={cockpit.format(_("Usage of $0"), block)}
aria-valuetext={labelText}>
<div className="usage-bar-indicator usage-bar-other" aria-hidden="true" style={{ width: total_fraction * 100 + "%" }} />
<div className="usage-bar-indicator" style={{ insetInlineStart: off_fraction * 100 + "%", width: fraction * 100 + "%" }} />
</div>
</div>);
};
the same table column, and also works well with the tests.
*/
export const StorageSize = ({ size }) => {
return (
<div>
<span className="usage-text pf-v6-u-text-nowrap">
{utils.fmt_size(size)}
</span>
<div className="usage-bar usage-bar-short usage-bar-empty" />
</div>);
};
export const StorageMenuItem = ({ onClick, danger, excuse, children, isDisabled }) => (
<DropdownItem className={danger && !excuse ? " delete-resource-dangerous" : ""}
description={excuse}
isDisabled={isDisabled || !!excuse}
onClick={checked(onClick, null, excuse)}>
{children}
</DropdownItem>
);
export const StorageBarMenu = ({ label, isKebab, menuItems }) => {
const [isOpen, setIsOpen] = useState(false);
if (!client.superuser.allowed)
return null;
const onToggleClick = (event) => {
setIsOpen(!isOpen);
};
const onSelect = (event) => {
setIsOpen(false);
};
const toggle = ref => (
<MenuToggle
ref={ref}
variant="plain"
onClick={onToggleClick}
isExpanded={isOpen}
aria-label={label}>
{isKebab ? <EllipsisVIcon /> : <Icon size="lg"><BarsIcon /></Icon>}
</MenuToggle>
);
return (
<Dropdown isOpen={isOpen}
onSelect={onSelect}
onOpenChange={isOpen => setIsOpen(isOpen)}
toggle={toggle}
popperProps={{ position: "right" }}
shouldFocusToggleOnSelect>
{menuItems}
</Dropdown>);
};