@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
463 lines (461 loc) • 18.1 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Typography from '@mui/material/Typography';
import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import AddIcon from '@mui/icons-material/Add';
import { makeStyles, withStyles } from 'tss-react/mui';
import { ConditionalLoadingState } from '../LoadingState/LoadingState';
import TableContainer from '@mui/material/TableContainer';
import Table from '@mui/material/Table';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import TableCell from '@mui/material/TableCell';
import { AsDayMonthDateTime } from '../VersionList';
import EmptyState from '../EmptyState/EmptyState';
import InstallPluginDialog from '../MarketplaceDialog';
import IconButton from '@mui/material/IconButton';
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
import Popover from '@mui/material/Popover';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import { useDispatch } from 'react-redux';
import { fetchInstalledMarketplacePlugins } from '../../services/marketplace';
import { showErrorDialog } from '../../state/reducers/dialogs/error';
import { getUserPermissions } from '../../services/security';
import {
emitSystemEvent,
pluginInstalled,
pluginUninstalled,
showSystemNotification
} from '../../state/actions/system';
import GlobalAppToolbar from '../GlobalAppToolbar';
import { useSelection } from '../../hooks/useSelection';
import { useActiveSiteId } from '../../hooks/useActiveSiteId';
import { useMount } from '../../hooks/useMount';
import { useEnhancedDialogState } from '../../hooks/useEnhancedDialogState';
import { useWithPendingChangesCloseRequest } from '../../hooks/useWithPendingChangesCloseRequest';
import { batchActions } from '../../state/actions/misc';
import Link from '@mui/material/Link';
import { createPresenceTable } from '../../utils/array';
import ListSubheader from '@mui/material/ListSubheader';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import TableBody from '@mui/material/TableBody';
import DeleteIcon from '@mui/icons-material/DeleteOutline';
import UninstallPluginDialog from '../DeletePluginDialog';
import SettingsRoundedIcon from '@mui/icons-material/SettingsOutlined';
import { PluginConfigDialog } from '../PluginConfigDialog';
const messages = defineMessages({
pluginInstalled: {
id: 'pluginManagement.pluginInstalled',
defaultMessage: 'Plugin installed successfully'
},
pluginConfigUpdated: {
id: 'pluginManagement.pluginConfigUpdated',
defaultMessage: 'Plugin configuration updated successfully'
},
pluginUninstalled: {
id: 'pluginManagement.pluginUninstalled',
defaultMessage: 'Plugin uninstalled successfully'
}
});
const styles = makeStyles()(() => ({
table: {
minWidth: 650
},
actions: {
width: '150px',
padding: '5px 20px'
}
}));
const StyledTableCell = withStyles(TableCell, () => ({
root: {
padding: '5px'
}
}));
export const PluginManagement = (props) => {
const { embedded = false, showAppsButton = !embedded } = props;
const { classes } = styles();
const dispatch = useDispatch();
const siteId = useActiveSiteId();
const { formatMessage } = useIntl();
const [plugins, setPlugins] = useState(null);
const [permissions, setPermissions] = useState(null);
const [openMarketPlaceDialog, setOpenMarketPlaceDialog] = useState(null);
const listPluginsPermission =
permissions === null || permissions === void 0 ? void 0 : permissions.includes('list_plugins');
const installPluginsPermission =
permissions === null || permissions === void 0 ? void 0 : permissions.includes('install_plugins');
const [anchorEl, setAnchorEl] = React.useState(null);
const [pluginFiles, setPluginFiles] = React.useState(null);
const [installedPluginsLookup, setInstalledPluginsLookup] = useState();
const locale = useSelection((state) => state.uiConfig.locale);
const deletePluginDialogState = useEnhancedDialogState();
const configPluginDialogState = useEnhancedDialogState();
const onWithPendingChangesCloseRequest = useWithPendingChangesCloseRequest(configPluginDialogState.onResetState);
const [pluginToDelete, setPluginToDelete] = useState(null);
const [pluginToConfig, setPluginToConfig] = useState(null);
useMount(() => {
getUserPermissions(siteId, '/').subscribe((permissions) => {
setPermissions(permissions);
});
});
const refresh = useCallback(
() =>
fetchInstalledMarketplacePlugins(siteId).subscribe(
(plugins) => {
setPlugins(plugins);
setInstalledPluginsLookup(
createPresenceTable(
plugins.map((plugin) => plugin.id),
true
)
);
},
(error) => {
dispatch(
showErrorDialog({
error
})
);
}
),
[dispatch, siteId]
);
useEffect(() => {
if (listPluginsPermission && siteId) {
refresh();
}
}, [dispatch, listPluginsPermission, refresh, siteId]);
const onSearchPlugin = () => {
setOpenMarketPlaceDialog({ installPermission: installPluginsPermission });
};
const onInstallMarketplacePlugin = (plugin) => {
dispatch(
batchActions([
showSystemNotification({ message: formatMessage(messages.pluginInstalled) }),
emitSystemEvent(pluginInstalled(plugin))
])
);
setInstalledPluginsLookup(Object.assign(Object.assign({}, installedPluginsLookup), { [plugin.id]: true }));
refresh();
};
const onCloseMarketplaceDialog = () => {
setOpenMarketPlaceDialog(null);
};
const showPluginFiles = (event, plugin) => {
setPluginFiles(plugin);
setAnchorEl(event.currentTarget);
};
const closePluginFilesPopover = () => {
setAnchorEl(null);
};
const onDeletePlugin = () => {
dispatch(
batchActions([
showSystemNotification({
message: formatMessage(messages.pluginUninstalled)
}),
emitSystemEvent(pluginUninstalled())
])
);
deletePluginDialogState.onResetState();
refresh();
};
const onSavedPluginConfig = () => {
dispatch(
showSystemNotification({
message: formatMessage(messages.pluginConfigUpdated)
})
);
configPluginDialogState.onResetState();
};
const onEditPluginConfig = (plugin) => {
setPluginToConfig(plugin.id);
configPluginDialogState.onOpen();
};
return React.createElement(
Paper,
{ elevation: 0 },
React.createElement(GlobalAppToolbar, {
title:
!embedded &&
React.createElement(FormattedMessage, {
id: 'globalMenu.pluginManagementEntryLabel',
defaultMessage: 'Plugin Management'
}),
showAppsButton: showAppsButton,
showHamburgerMenuButton: !embedded,
styles: embedded && {
leftContent: {
marginLeft: 0
}
},
leftContent: React.createElement(
Button,
{
startIcon: React.createElement(AddIcon, null),
variant: 'outlined',
color: 'primary',
onClick: onSearchPlugin,
disabled: permissions === null || listPluginsPermission === false
},
installPluginsPermission
? React.createElement(FormattedMessage, {
id: 'pluginManagement.searchPlugin',
defaultMessage: 'Search & install'
})
: React.createElement(FormattedMessage, { id: 'words.search', defaultMessage: 'Search' })
)
}),
permissions && listPluginsPermission === false
? React.createElement(EmptyState, {
title: React.createElement(FormattedMessage, {
id: 'pluginManagement.listPluginPermission',
defaultMessage: "You don't have enough permissions to see the list of plugins"
})
})
: React.createElement(
ConditionalLoadingState,
{ isLoading: plugins === null },
React.createElement(
TableContainer,
null,
React.createElement(
Table,
{ className: classes.table },
React.createElement(
TableHead,
null,
React.createElement(
TableRow,
null,
React.createElement(
TableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.id', defaultMessage: 'Id' })
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.version', defaultMessage: 'Version' })
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.url', defaultMessage: 'Url' })
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.files', defaultMessage: 'Files' })
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, {
id: 'pluginManagement.installationDate',
defaultMessage: 'Installation Date'
})
)
),
React.createElement(TableCell, { align: 'center', className: classes.actions })
)
),
React.createElement(
TableBody,
null,
plugins === null || plugins === void 0
? void 0
: plugins.map((plugin) =>
React.createElement(
TableRow,
{ key: plugin.id },
React.createElement(TableCell, { component: 'th', id: plugin.id, scope: 'row' }, plugin.id),
React.createElement(
StyledTableCell,
{ align: 'left' },
plugin.version.major,
'.',
plugin.version.minor,
'.',
plugin.version.patch
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(Link, { href: plugin.pluginUrl, target: '_blank' }, plugin.pluginUrl)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
plugin.files.length,
React.createElement(
IconButton,
{ onClick: (e) => showPluginFiles(e, plugin), size: 'small' },
React.createElement(ExpandMoreRoundedIcon, null)
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(AsDayMonthDateTime, { date: plugin.installationDate, locale: locale })
),
React.createElement(
TableCell,
{ align: 'right', className: classes.actions },
React.createElement(
IconButton,
{
onClick: () => {
onEditPluginConfig(plugin);
},
color: 'primary'
},
React.createElement(SettingsRoundedIcon, null)
),
React.createElement(
IconButton,
{
onClick: () => {
setPluginToDelete(plugin.id);
deletePluginDialogState.onOpen();
},
color: 'primary'
},
React.createElement(DeleteIcon, null)
)
)
)
)
)
)
),
(plugins === null || plugins === void 0 ? void 0 : plugins.length) === 0 &&
React.createElement(EmptyState, {
title: React.createElement(FormattedMessage, {
id: 'pluginManagement.emptyList',
defaultMessage: 'There are no plugins installed'
})
})
),
React.createElement(InstallPluginDialog, {
open: Boolean(openMarketPlaceDialog),
onClose: onCloseMarketplaceDialog,
onInstall: onInstallMarketplacePlugin,
installedPlugins: installedPluginsLookup,
installPermission:
openMarketPlaceDialog === null || openMarketPlaceDialog === void 0
? void 0
: openMarketPlaceDialog.installPermission
}),
React.createElement(UninstallPluginDialog, {
open: deletePluginDialogState.open,
onClose: deletePluginDialogState.onResetState,
isSubmitting: deletePluginDialogState.isSubmitting,
hasPendingChanges: deletePluginDialogState.hasPendingChanges,
isMinimized: deletePluginDialogState.isMinimized,
onSubmittingAndOrPendingChange: deletePluginDialogState.onSubmittingAndOrPendingChange,
pluginId: pluginToDelete,
onComplete: onDeletePlugin
}),
React.createElement(PluginConfigDialog, {
open: configPluginDialogState.open,
onClose: configPluginDialogState.onResetState,
isSubmitting: configPluginDialogState.isSubmitting,
hasPendingChanges: configPluginDialogState.hasPendingChanges,
isMinimized: configPluginDialogState.isMinimized,
onMinimize: configPluginDialogState.onMinimize,
onMaximize: configPluginDialogState.onMaximize,
isFullScreen: configPluginDialogState.isFullScreen,
onFullScreen: configPluginDialogState.onFullScreen,
onCancelFullScreen: configPluginDialogState.onCancelFullScreen,
onSubmittingAndOrPendingChange: configPluginDialogState.onSubmittingAndOrPendingChange,
pluginId: pluginToConfig,
onSaved: onSavedPluginConfig,
onWithPendingChangesCloseRequest: onWithPendingChangesCloseRequest
}),
React.createElement(
Popover,
{
open: Boolean(anchorEl),
anchorEl: anchorEl,
onClose: closePluginFilesPopover,
anchorOrigin: {
vertical: 'bottom',
horizontal: 'center'
},
transformOrigin: {
vertical: 'top',
horizontal: 'center'
}
},
React.createElement(
List,
{
dense: true,
subheader: React.createElement(
ListSubheader,
null,
React.createElement(FormattedMessage, { id: 'words.files', defaultMessage: 'Files' })
)
},
pluginFiles === null || pluginFiles === void 0
? void 0
: pluginFiles.files.map((file, i) =>
React.createElement(ListItem, { key: i }, React.createElement(ListItemText, { primary: file.path }))
)
)
)
);
};
export default PluginManagement;