UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

463 lines (461 loc) 18.1 kB
/* * 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;