Plugins: Improve update all modal UX (#93448)

pull/93786/head
Hugo Kiyodi Oshiro 8 months ago committed by GitHub
parent cdbc04ab2b
commit 368fc0f120
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 200
      public/app/features/plugins/admin/components/UpdateAllModal.tsx
  2. 208
      public/app/features/plugins/admin/components/UpdateAllModalBody.tsx
  3. 5
      public/locales/en-US/grafana.json
  4. 5
      public/locales/pseudo-LOCALE/grafana.json

@ -1,14 +1,13 @@
import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { Checkbox, ConfirmModal, EmptyState, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ConfirmModal } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useInstall, useInstallStatus } from '../state/hooks';
import { CatalogPlugin } from '../types';
import { UpdateModalBody } from './UpdateAllModalBody';
const PLUGINS_UPDATE_ALL_INTERACTION_EVENT_NAME = 'plugins_update_all_clicked';
type UpdateError = {
@ -16,150 +15,6 @@ type UpdateError = {
message: string;
};
function getIcon({
id,
inProgress,
errorMap,
selectedPlugins,
}: {
id: string;
inProgress: boolean;
errorMap: Map<string, UpdateError>;
selectedPlugins?: Set<string>;
}) {
if (errorMap && errorMap.has(id)) {
return (
<Tooltip
content={`${t('plugins.catalog.update-all.error', 'Error updating plugin:')} ${errorMap.get(id)?.message}`}
>
<Icon size="xl" name="exclamation-circle" />
</Tooltip>
);
}
if (inProgress && selectedPlugins?.has(id)) {
return <Spinner />;
}
return '';
}
const getStyles = (theme: GrafanaTheme2) => ({
table: css({
marginTop: theme.spacing(2),
width: '100%',
borderCollapse: 'collapse',
}),
tableRow: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
td: {
paddingRight: theme.spacing(1),
},
}),
icon: css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}),
header: css({
textAlign: 'left',
padding: theme.spacing(1),
borderBottom: `2px solid ${theme.colors.border.strong}`,
th: {
paddingRight: theme.spacing(1),
},
}),
data: css({
padding: '10px',
}),
footer: css({
fontSize: theme.typography.bodySmall.fontSize,
marginTop: theme.spacing(3),
}),
noPluginsMessage: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
}),
tableContainer: css({
overflowY: 'auto',
overflowX: 'hidden',
height: theme.spacing(32),
}),
modalContainer: css({
height: theme.spacing(41),
}),
});
type ModalBodyProps = {
plugins: CatalogPlugin[];
inProgress: boolean;
selectedPlugins?: Set<string>;
onCheckboxChange: (id: string) => void;
errorMap: Map<string, UpdateError>;
};
const ModalBody = ({ plugins, inProgress, selectedPlugins, onCheckboxChange, errorMap }: ModalBodyProps) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.modalContainer}>
{plugins.length === 0 ? (
<EmptyState
variant="completed"
message={t('plugins.catalog.update-all.all-plugins-updated', 'All plugins updated!')}
/>
) : (
<>
<div>
<Trans i18nKey="plugins.catalog.update-all.header">The following plugins have update available</Trans>
</div>
<div className={styles.tableContainer}>
<table className={styles.table}>
<thead className={styles.header}>
<tr>
<th>
<Trans i18nKey="plugins.catalog.update-all.update-header">Update</Trans>
</th>
<th>
<Trans i18nKey="plugins.catalog.update-all.name-header">Name</Trans>
</th>
<th>
<Trans i18nKey="plugins.catalog.update-all.installed-header">Installed</Trans>
</th>
<th>
<Trans i18nKey="plugins.catalog.update-all.available-header">Available</Trans>
</th>
<th></th>
</tr>
</thead>
<tbody>
{plugins.map(({ id, name, installedVersion, latestVersion }: CatalogPlugin) => (
<tr key={id} className={styles.tableRow}>
<td>
<Checkbox onChange={() => onCheckboxChange(id)} value={selectedPlugins?.has(id)} />
</td>
<td>{name}</td>
<td>{installedVersion}</td>
<td>{latestVersion}</td>
<td className={styles.icon}>{getIcon({ id, inProgress, errorMap, selectedPlugins })}</td>
</tr>
))}
</tbody>
</table>
</div>
{config.pluginAdminExternalManageEnabled && config.featureToggles.managedPluginsInstall && (
<footer className={styles.footer}>
<Trans i18nKey="plugins.catalog.update-all.cloud-update-message">
* It may take a few minutes for the plugins to be available for usage.
</Trans>
</footer>
)}
</>
)}
</div>
);
};
type Props = {
isOpen: boolean;
isLoading: boolean;
@ -173,10 +28,19 @@ export const UpdateAllModal = ({ isOpen, onDismiss, isLoading, plugins }: Props)
const [errorMap, setErrorMap] = useState(new Map<string, UpdateError>());
const [inProgress, setInProgress] = useState(false);
const [selectedPlugins, setSelectedPlugins] = useState<Set<string>>();
const initialPluginsRef = useRef(plugins);
const pluginsSet = useMemo(() => new Set(plugins.map((plugin) => plugin.id)), [plugins]);
const installsRemaining = plugins.length;
// Since the plugins comes from the store and changes every time we update a plugin,
// we need to keep track of the initial plugins.
useEffect(() => {
if (initialPluginsRef.current.length === 0) {
initialPluginsRef.current = [...plugins];
}
}, [plugins]);
// Updates the component state on every plugins change, since the installation will change the store content
useEffect(() => {
if (inProgress) {
@ -245,6 +109,7 @@ export const UpdateAllModal = ({ isOpen, onDismiss, isLoading, plugins }: Props)
};
const onDismissClick = () => {
initialPluginsRef.current = [];
setErrorMap(new Map());
setInProgress(false);
setSelectedPlugins(undefined);
@ -277,8 +142,9 @@ export const UpdateAllModal = ({ isOpen, onDismiss, isLoading, plugins }: Props)
isOpen={isOpen}
title={t('plugins.catalog.update-all.modal-title', 'Update Plugins')}
body={
<ModalBody
plugins={plugins}
<UpdateModalBody
plugins={initialPluginsRef.current}
pluginsNotInstalled={pluginsSet}
inProgress={inProgress}
errorMap={errorMap}
onCheckboxChange={onCheckboxChange}
@ -287,14 +153,34 @@ export const UpdateAllModal = ({ isOpen, onDismiss, isLoading, plugins }: Props)
}
onConfirm={installsRemaining > 0 ? onConfirm : onDismissClick}
onDismiss={onDismissClick}
disabled={pluginsSelected === 0 || inProgress}
confirmText={
installsRemaining > 0
? `${t('plugins.catalog.update-all.modal-confirmation', 'Update')} (${pluginsSelected})`
: t('plugins.catalog.update-all.modal-dismiss', 'Close')
}
disabled={shouldDisableConfirm(inProgress, installsRemaining, pluginsSelected)}
confirmText={getConfirmationText(installsRemaining, inProgress, pluginsSelected)}
confirmButtonVariant="primary"
/>
);
};
function getConfirmationText(installsRemaining: number, inProgress: boolean, pluginsSelected: number) {
if (inProgress) {
return t('plugins.catalog.update-all.modal-in-progress', 'Updating...');
}
if (installsRemaining > 0) {
return t('plugins.catalog.update-all.modal-confirmation', 'Update') + ` (${pluginsSelected})`;
}
return t('plugins.catalog.update-all.modal-dismiss', 'Close');
}
function shouldDisableConfirm(inProgress: boolean, installsRemaining: number, pluginsSelected: number) {
if (inProgress) {
return true;
}
if (installsRemaining > 0 && pluginsSelected === 0) {
return true;
}
return false;
}
export default UpdateAllModal;

@ -0,0 +1,208 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Checkbox, EmptyState, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { CatalogPlugin } from '../types';
type UpdateError = {
id: string;
message: string;
};
const getStyles = (theme: GrafanaTheme2) => ({
table: css({
marginTop: theme.spacing(2),
width: '100%',
borderCollapse: 'collapse',
}),
tableRow: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
td: {
paddingRight: theme.spacing(1),
},
}),
icon: css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}),
header: css({
textAlign: 'left',
padding: theme.spacing(1),
borderBottom: `2px solid ${theme.colors.border.strong}`,
th: {
paddingRight: theme.spacing(1),
},
}),
data: css({
padding: '10px',
}),
footer: css({
fontSize: theme.typography.bodySmall.fontSize,
marginTop: theme.spacing(3),
}),
noPluginsMessage: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
}),
tableContainer: css({
overflowY: 'auto',
overflowX: 'hidden',
maxHeight: theme.spacing(41),
marginBottom: theme.spacing(2),
}),
errorIcon: css({
color: theme.colors.error.main,
}),
successIcon: css({
color: theme.colors.success.main,
}),
pluginsInstalled: css({
svg: {
marginRight: theme.spacing(1),
},
}),
});
const StatusIcon = ({
id,
inProgress,
isSelected,
isInstalled,
errorMap,
}: {
id: string;
inProgress: boolean;
isSelected: boolean;
isInstalled: boolean;
errorMap: Map<string, UpdateError>;
}) => {
const styles = useStyles2(getStyles);
if (errorMap && errorMap.has(id)) {
return (
<Tooltip
content={`${t('plugins.catalog.update-all.error', 'Error updating plugin:')} ${errorMap.get(id)?.message}`}
>
<Icon className={styles.errorIcon} size="xl" name="exclamation-triangle" />
</Tooltip>
);
}
if (isInstalled) {
return <Icon className={styles.successIcon} size="xl" name="check" />;
}
if (inProgress && isSelected) {
return <Spinner />;
}
return '';
};
type Props = {
plugins: CatalogPlugin[];
pluginsNotInstalled: Set<string>;
inProgress: boolean;
selectedPlugins?: Set<string>;
onCheckboxChange: (id: string) => void;
errorMap: Map<string, UpdateError>;
};
export const UpdateModalBody = ({
plugins,
pluginsNotInstalled,
inProgress,
selectedPlugins,
onCheckboxChange,
errorMap,
}: Props) => {
const styles = useStyles2(getStyles);
const numberInstalled = plugins.length - pluginsNotInstalled.size;
const installationFinished = plugins.length !== pluginsNotInstalled.size && !inProgress;
return (
<div>
{plugins.length === 0 ? (
<EmptyState
variant="completed"
message={t('plugins.catalog.update-all.all-plugins-updated', 'All plugins updated!')}
/>
) : (
<>
<div>
<Trans i18nKey="plugins.catalog.update-all.header">The following plugins have update available</Trans>
</div>
<div className={styles.tableContainer}>
<table className={styles.table}>
<thead className={styles.header}>
<tr>
<th>
<Trans i18nKey="plugins.catalog.update-all.update-header">Update</Trans>
</th>
<th>
<Trans i18nKey="plugins.catalog.update-all.name-header">Name</Trans>
</th>
<th>
<Trans i18nKey="plugins.catalog.update-all.installed-header">Installed</Trans>
</th>
<th>
<Trans i18nKey="plugins.catalog.update-all.available-header">Available</Trans>
</th>
<th></th>
</tr>
</thead>
<tbody>
{plugins.map(({ id, name, installedVersion, latestVersion }: CatalogPlugin) => (
<tr key={id} className={styles.tableRow}>
<td>
<Checkbox
onChange={() => onCheckboxChange(id)}
value={selectedPlugins?.has(id)}
disabled={!pluginsNotInstalled.has(id)}
/>
</td>
<td>{name}</td>
<td>{installedVersion}</td>
<td>{latestVersion}</td>
<td className={styles.icon}>
<StatusIcon
id={id}
inProgress={inProgress}
isSelected={selectedPlugins?.has(id) ?? false}
isInstalled={!pluginsNotInstalled.has(id)}
errorMap={errorMap}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
{numberInstalled > 0 && installationFinished && (
<div className={styles.pluginsInstalled}>
<Icon className={styles.successIcon} size="lg" name="check" />
{`${numberInstalled} ${t('plugins.catalog.update-all.update-status-text', 'plugins updated')}`}
</div>
)}
{errorMap.size > 0 && installationFinished && (
<div className={styles.pluginsInstalled}>
<Icon className={styles.errorIcon} size="lg" name="exclamation-triangle" />
{`${errorMap.size} ${t('plugins.catalog.update-all.error-status-text', 'failed - see error messages')}`}
</div>
)}
{config.pluginAdminExternalManageEnabled && config.featureToggles.managedPluginsInstall && (
<footer className={styles.footer}>
<Trans i18nKey="plugins.catalog.update-all.cloud-update-message">
* It may take a few minutes for the plugins to be available for usage.
</Trans>
</footer>
)}
</>
)}
</div>
);
};

@ -1926,13 +1926,16 @@
"button": "Update all",
"cloud-update-message": "* It may take a few minutes for the plugins to be available for usage.",
"error": "Error updating plugin:",
"error-status-text": "failed - see error messages",
"header": "The following plugins have update available",
"installed-header": "Installed",
"modal-confirmation": "Update",
"modal-dismiss": "Close",
"modal-in-progress": "Updating...",
"modal-title": "Update Plugins",
"name-header": "Name",
"update-header": "Update"
"update-header": "Update",
"update-status-text": "plugins updated"
}
},
"details": {

@ -1926,13 +1926,16 @@
"button": "Ůpđäŧę äľľ",
"cloud-update-message": "* Ĩŧ mäy ŧäĸę ä ƒęŵ mįʼnūŧęş ƒőř ŧĥę pľūģįʼnş ŧő þę äväįľäþľę ƒőř ūşäģę.",
"error": "Ēřřőř ūpđäŧįʼnģ pľūģįʼn:",
"error-status-text": "ƒäįľęđ - şęę ęřřőř męşşäģęş",
"header": "Ŧĥę ƒőľľőŵįʼnģ pľūģįʼnş ĥävę ūpđäŧę äväįľäþľę",
"installed-header": "Ĩʼnşŧäľľęđ",
"modal-confirmation": "Ůpđäŧę",
"modal-dismiss": "Cľőşę",
"modal-in-progress": "Ůpđäŧįʼnģ...",
"modal-title": "Ůpđäŧę Pľūģįʼnş",
"name-header": "Ńämę",
"update-header": "Ůpđäŧę"
"update-header": "Ůpđäŧę",
"update-status-text": "pľūģįʼnş ūpđäŧęđ"
}
},
"details": {

Loading…
Cancel
Save