Line data Source code
1 : /*
2 : * Copyright (C) 2016 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 113 : import React, { useState } from 'react';
7 :
8 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
9 : import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
10 : import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle/index.js';
11 : import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
12 : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
13 : import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
14 : import { BarsIcon, EllipsisVIcon } from '@patternfly/react-icons';
15 :
16 113 : import cockpit from "cockpit";
17 : import * as utils from "./utils.js";
18 : import client from "./client.js";
19 :
20 : import { dialog_open } from "./dialog.jsx";
21 :
22 113 : const _ = cockpit.gettext;
23 :
24 : /* StorageControl - a button or similar that triggers
25 : * a privileged action.
26 : *
27 : * It can be disabled and will show a tooltip then. It will
28 : * automatically disappear when the logged in user doesn't
29 : * have permission.
30 : *
31 : * Properties:
32 : *
33 : * - excuse: If set, the button/link is disabled and will show the
34 : * excuse in a tooltip.
35 : */
36 :
37 113 : class StorageControl extends React.Component {
38 85 : render() {
39 85 : const excuse = this.props.excuse;
40 85 : if (!client.superuser.allowed)
41 8 : return <div />;
42 :
43 54 : if (excuse) {
44 54 : return (
45 54 : <Tooltip id="tip-storage" content={excuse}
46 54 : position={this.props.excuse_placement || TooltipPosition.top}>
47 54 : <span>
48 54 : { this.props.content(excuse) }
49 54 : </span>
50 54 : </Tooltip>
51 : );
52 54 : } else {
53 85 : return this.props.content();
54 85 : }
55 85 : }
56 113 : }
57 :
58 107 : function checked(callback, setSpinning, excuse) {
59 93 : return function (event) {
60 93 : if (!event)
61 93 : return;
62 :
63 : // only consider primary mouse button for clicks
64 93 : if (event.type === 'click' && event.button !== 0)
65 93 : return;
66 :
67 : // only consider enter button for keyboard events
68 3 : if (event.type === 'KeyDown' && event.key !== "Enter")
69 93 : return;
70 :
71 93 : event.stopPropagation();
72 :
73 3 : if (excuse) {
74 3 : dialog_open({
75 3 : Title: _("Sorry"),
76 3 : Body: excuse
77 3 : });
78 3 : return;
79 3 : }
80 :
81 93 : const promise = client.run(callback);
82 93 : if (promise) {
83 93 : if (setSpinning)
84 54 : setSpinning(true);
85 93 : promise.finally(() => {
86 93 : if (setSpinning)
87 54 : setSpinning(false);
88 93 : });
89 0 : promise.catch(function (error) {
90 0 : console.warn(error.toString());
91 0 : dialog_open({
92 0 : Title: _("Error"),
93 0 : Body: error.toString()
94 0 : });
95 0 : });
96 93 : }
97 93 : };
98 107 : }
99 :
100 81 : export const StorageButton = ({ id, kind, excuse, onClick, children, ariaLabel, spinner }) => {
101 81 : const [spinning, setSpinning] = useState(false);
102 :
103 81 : return <StorageControl excuse={excuse}
104 81 : content={excuse => (
105 81 : <Button id={id}
106 81 : aria-label={ariaLabel}
107 81 : onClick={checked(onClick, setSpinning)}
108 81 : variant={kind || "secondary"}
109 11 : isDisabled={!!excuse || (spinner && spinning)}
110 11 : isLoading={spinner ? spinning : undefined}>
111 81 : {children}
112 81 : </Button>
113 81 : )} />;
114 81 : };
115 :
116 81 : export const StorageLink = ({ id, excuse, onClick, children }) => (
117 81 : <StorageControl excuse={excuse}
118 81 : content={excuse => (
119 81 : <Button onClick={checked(onClick)}
120 81 : variant="link"
121 81 : isInline
122 81 : isDisabled={!!excuse}>
123 81 : {children}
124 81 : </Button>
125 81 : )} />
126 : );
127 :
128 : // StorageOnOff - OnOff switch for asynchronous actions.
129 : //
130 :
131 113 : export class StorageOnOff extends React.Component {
132 17 : constructor() {
133 17 : super();
134 17 : this.state = { promise: null };
135 17 : }
136 :
137 17 : render() {
138 17 : const self = this;
139 :
140 2 : function onChange(_event, val) {
141 2 : const promise = self.props.onChange(val);
142 2 : if (promise) {
143 0 : promise.catch(error => {
144 0 : dialog_open({
145 0 : Title: _("Error"),
146 0 : Body: error.toString()
147 0 : });
148 0 : })
149 2 : .finally(() => { self.setState({ promise: null }) });
150 2 : }
151 :
152 2 : self.setState({ promise, promise_goal_state: val });
153 2 : }
154 :
155 17 : return (
156 17 : <StorageControl excuse={this.props.excuse}
157 17 : content={(excuse) => (
158 17 : <Switch isChecked={this.state.promise
159 6 : ? this.state.promise_goal_state
160 17 : : this.props.state}
161 17 : aria-label={this.props['aria-label']}
162 16 : isDisabled={!!(excuse || this.state.promise)}
163 17 : onChange={onChange} />
164 17 : )} />
165 : );
166 17 : }
167 113 : }
168 :
169 : /* Render a usage bar showing props.stats[0] out of props.stats[1]
170 : * bytes in use. If the ratio is above props.critical, the bar will be
171 : * in a dangerous color.
172 : */
173 :
174 101 : export const StorageUsageBar = ({ stats, critical, block, offset, total, short }) => {
175 101 : if (!stats)
176 13 : return null;
177 :
178 101 : const fraction = stats[0] / stats[1];
179 101 : const off_fraction = offset / stats[1];
180 101 : const total_fraction = total / stats[1];
181 101 : const labelText = utils.format_fsys_usage(stats[0], stats[1]);
182 :
183 101 : return (
184 101 : <div>
185 101 : <span className="usage-text pf-v6-u-text-nowrap">
186 101 : {labelText}
187 101 : </span>
188 10 : <div className={"usage-bar" + (fraction > critical ? " usage-bar-danger" : "") + (short ? " usage-bar-short" : "")}
189 101 : role="progressbar"
190 101 : aria-valuemin={0} aria-valuemax={stats[1]} aria-valuenow={stats[0]}
191 101 : aria-label={cockpit.format(_("Usage of $0"), block)}
192 101 : aria-valuetext={labelText}>
193 101 : <div className="usage-bar-indicator usage-bar-other" aria-hidden="true" style={{ width: total_fraction * 100 + "%" }} />
194 101 : <div className="usage-bar-indicator" style={{ insetInlineStart: off_fraction * 100 + "%", width: fraction * 100 + "%" }} />
195 101 : </div>
196 101 : </div>);
197 101 : };
198 :
199 : /* Render a static size that goes well with a short StorageusageBar in
200 : the same table column, and also works well with the tests.
201 : */
202 :
203 106 : export const StorageSize = ({ size }) => {
204 106 : return (
205 106 : <div>
206 106 : <span className="usage-text pf-v6-u-text-nowrap">
207 106 : {utils.fmt_size(size)}
208 106 : </span>
209 106 : <div className="usage-bar usage-bar-short usage-bar-empty" />
210 106 : </div>);
211 106 : };
212 :
213 85 : export const StorageMenuItem = ({ onClick, danger, excuse, children, isDisabled }) => (
214 46 : <DropdownItem className={danger && !excuse ? " delete-resource-dangerous" : ""}
215 85 : description={excuse}
216 85 : isDisabled={isDisabled || !!excuse}
217 85 : onClick={checked(onClick, null, excuse)}>
218 85 : {children}
219 85 : </DropdownItem>
220 : );
221 :
222 112 : export const StorageBarMenu = ({ label, isKebab, menuItems }) => {
223 112 : const [isOpen, setIsOpen] = useState(false);
224 :
225 112 : if (!client.superuser.allowed)
226 13 : return null;
227 :
228 85 : const onToggleClick = (event) => {
229 85 : setIsOpen(!isOpen);
230 85 : };
231 :
232 84 : const onSelect = (event) => {
233 84 : setIsOpen(false);
234 84 : };
235 :
236 112 : const toggle = ref => (
237 112 : <MenuToggle
238 112 : ref={ref}
239 112 : variant="plain"
240 112 : onClick={onToggleClick}
241 112 : isExpanded={isOpen}
242 112 : aria-label={label}>
243 112 : {isKebab ? <EllipsisVIcon /> : <Icon size="lg"><BarsIcon /></Icon>}
244 112 : </MenuToggle>
245 : );
246 :
247 112 : return (
248 112 : <Dropdown isOpen={isOpen}
249 112 : onSelect={onSelect}
250 0 : onOpenChange={isOpen => setIsOpen(isOpen)}
251 112 : toggle={toggle}
252 112 : popperProps={{ position: "right" }}
253 112 : shouldFocusToggleOnSelect>
254 112 : {menuItems}
255 112 : </Dropdown>);
256 112 : };
|