1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-22 18:50:08 +03:00

F #5422: Add actions & tabs to apps (#1589)

This commit is contained in:
Sergio Betanzos 2021-11-18 16:33:34 +01:00 committed by GitHub
parent 083f86a21f
commit e84d1d2be4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 507 additions and 18 deletions

View File

@ -0,0 +1,78 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo } from 'react'
// import { useHistory } from 'react-router-dom'
import { RefreshDouble } from 'iconoir-react'
import { useAuth } from 'client/features/Auth'
import { useMarketplaceAppApi } from 'client/features/One'
import { Translate } from 'client/components/HOC'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
// import { PATH } from 'client/apps/sunstone/routesOne'
import { T, MARKETPLACE_APP_ACTIONS } from 'client/constants'
const MessageToConfirmAction = rows => {
const names = rows?.map?.(({ original }) => original?.NAME)
return (
<>
<p>
<Translate word={T.Apps} />
{`: ${names.join(', ')}`}
</p>
<p>
<Translate word={T.DoYouWantProceed} />
</p>
</>
)
}
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
const Actions = () => {
const { view, getResourceView } = useAuth()
const { getMarketplaceApps } = useMarketplaceAppApi()
const marketplaceAppActions = useMemo(() => createActions({
filters: getResourceView('MARKETPLACE-APP')?.actions,
actions: [
{
accessor: MARKETPLACE_APP_ACTIONS.REFRESH,
tooltip: T.Refresh,
icon: RefreshDouble,
action: async () => {
await getMarketplaceApps()
}
}
/* {
accessor: MARKETPLACE_APP_ACTIONS.CREATE_DIALOG,
tooltip: T.CreateMarketApp,
icon: AddSquare,
action: () => {
const path = PATH.STORAGE.MARKETPLACE_APPS.CREATE
history.push(path)
}
} */
]
}), [view])
return marketplaceAppActions
}
export default Actions

View File

@ -21,15 +21,20 @@ import { useFetch } from 'client/hooks'
import { useMarketplaceApp, useMarketplaceAppApi } from 'client/features/One'
import { SkeletonTable, EnhancedTable } from 'client/components/Tables'
import { createColumns } from 'client/components/Tables/Enhanced/Utils'
import MarketplaceAppColumns from 'client/components/Tables/MarketplaceApps/columns'
import MarketplaceAppRow from 'client/components/Tables/MarketplaceApps/row'
const MarketplaceAppsTable = () => {
const columns = useMemo(() => MarketplaceAppColumns, [])
const MarketplaceAppsTable = props => {
const { view, getResourceView, filterPool } = useAuth()
const columns = useMemo(() => createColumns({
filters: getResourceView('MARKETPLACE-APP')?.filters,
columns: MarketplaceAppColumns
}), [view])
const marketplaceApps = useMarketplaceApp()
const { getMarketplaceApps } = useMarketplaceAppApi()
const { filterPool } = useAuth()
const { status, fetchRequest, loading, reloading, STATUS } = useFetch(getMarketplaceApps)
const { INIT, PENDING } = STATUS
@ -47,6 +52,7 @@ const MarketplaceAppsTable = () => {
isLoading={loading || reloading}
getRowId={row => String(row.ID)}
RowComponent={MarketplaceAppRow}
{...props}
/>
)
}

View File

@ -0,0 +1,134 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useContext } from 'react'
import PropTypes from 'prop-types'
import { useMarketplaceAppApi } from 'client/features/One'
import { TabContext } from 'client/components/Tabs/TabProvider'
import { Permissions, Ownership, AttributePanel } from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/MarketplaceApp/Info/information'
import { Tr } from 'client/components/HOC'
import { getActionsAvailable, filterAttributes, jsonToXml } from 'client/models/Helper'
import { cloneObject, set } from 'client/utils'
import { T } from 'client/constants'
const HIDDEN_ATTRIBUTES_REG = /^(VMTEMPLATE64|APPTEMPLATE64)$/
const MarketplaceAppInfoTab = ({ tabProps = {} }) => {
const {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
attributes_panel: attributesPanel
} = tabProps
const { rename, changeOwnership, changePermissions, updateTemplate } = useMarketplaceAppApi()
const { handleRefetch, data: marketplaceApp = {} } = useContext(TabContext)
const { ID, UNAME, UID, GNAME, GID, PERMISSIONS, TEMPLATE } = marketplaceApp
const handleChangeOwnership = async newOwnership => {
const response = await changeOwnership(ID, newOwnership)
String(response) === String(ID) && (await handleRefetch?.())
}
const handleChangePermission = async newPermission => {
const response = await changePermissions(ID, newPermission)
String(response) === String(ID) && (await handleRefetch?.())
}
const handleRename = async (_, newName) => {
const response = await rename(ID, newName)
String(response) === String(ID) && (await handleRefetch?.())
}
const handleAttributeInXml = async (path, newValue) => {
const newTemplate = cloneObject(TEMPLATE)
set(newTemplate, path, newValue)
const xml = jsonToXml(newTemplate)
// 0: Replace the whole template
const response = await updateTemplate(ID, xml, 0)
String(response) === String(ID) && (await handleRefetch?.())
}
const getActions = actions => getActionsAvailable(actions)
const { attributes } = filterAttributes(TEMPLATE, { hidden: HIDDEN_ATTRIBUTES_REG })
return (
<div style={{
display: 'grid',
gap: '1em',
gridTemplateColumns: 'repeat(auto-fit, minmax(480px, 1fr))',
padding: '0.8em'
}}>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
handleRename={handleRename}
marketplaceApp={marketplaceApp}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
handleEdit={handleChangePermission}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
handleEdit={handleChangeOwnership}
/>
)}
{attributesPanel?.enabled && attributes && (
<AttributePanel
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
handleAdd={handleAttributeInXml}
handleEdit={handleAttributeInXml}
handleDelete={handleAttributeInXml}
/>
)}
</div>
)
}
MarketplaceAppInfoTab.propTypes = {
tabProps: PropTypes.object
}
MarketplaceAppInfoTab.displayName = 'MarketplaceAppInfoTab'
export default MarketplaceAppInfoTab

View File

@ -0,0 +1,80 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { generatePath } from 'react-router'
import { StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import { getType, getState } from 'client/models/MarketplaceApp'
import { timeToString, levelLockToString } from 'client/models/Helper'
import { prettyBytes } from 'client/utils'
import { T, MARKETPLACE_APP_ACTIONS } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
const InformationPanel = ({ marketplaceApp = {}, handleRename, actions }) => {
const { ID, NAME, REGTIME, LOCK, MARKETPLACE, MARKETPLACE_ID, SIZE, FORMAT, VERSION } = marketplaceApp
const typeName = getType(marketplaceApp)
const { name: stateName, color: stateColor } = getState(marketplaceApp)
const info = [
{ name: T.ID, value: ID },
{
name: T.Name,
value: NAME,
canEdit: actions?.includes?.(MARKETPLACE_APP_ACTIONS.RENAME),
handleEdit: handleRename
},
{
name: T.Marketplace,
value: `#${MARKETPLACE_ID} ${MARKETPLACE}`,
link: !Number.isNaN(+MARKETPLACE_ID) &&
generatePath(PATH.STORAGE.MARKETPLACES.DETAIL, { id: MARKETPLACE_ID })
},
{
name: T.StartTime,
value: timeToString(REGTIME)
},
{ name: T.Type, value: typeName },
{ name: T.Size, value: prettyBytes(SIZE, 'MB') },
{
name: T.State,
value: <StatusChip text={stateName} stateColor={stateColor} />
},
{ name: T.Locked, value: levelLockToString(LOCK?.LOCKED) },
{ name: T.Format, value: FORMAT },
{ name: T.Version, value: VERSION }
]
return (
<List
title={T.Information}
list={info}
containerProps={{ style: { gridRow: 'span 3' } }}
/>
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
handleRename: PropTypes.func,
marketplaceApp: PropTypes.object
}
export default InformationPanel

View File

@ -0,0 +1,70 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useContext, useMemo } from 'react'
import { Accordion, AccordionDetails, AccordionSummary } from '@mui/material'
import { NavArrowDown as ExpandMoreIcon } from 'iconoir-react'
import { TabContext } from 'client/components/Tabs/TabProvider'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
const parseTemplateInB64 = template => {
try {
return decodeURIComponent(escape(atob(template)))
} catch (e) { return {} }
}
const AppTemplateTab = () => {
const { data: marketplaceApp = {} } = useContext(TabContext)
const { TEMPLATE: { APPTEMPLATE64, VMTEMPLATE64 } } = marketplaceApp
const appTemplate = useMemo(() => parseTemplateInB64(APPTEMPLATE64), [APPTEMPLATE64])
const vmTemplate = useMemo(() => parseTemplateInB64(VMTEMPLATE64), [VMTEMPLATE64])
return (
<>
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Translate word={T.AppTemplate} />
</AccordionSummary>
<AccordionDetails>
<pre>
<code style={{ whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
{appTemplate}
</code>
</pre>
</AccordionDetails>
</Accordion>
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Translate word={T.VMTemplate} />
</AccordionSummary>
<AccordionDetails>
<pre>
<code style={{ whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
{vmTemplate}
</code>
</pre>
</AccordionDetails>
</Accordion>
</>
)
}
AppTemplateTab.displayName = 'AppTemplateTab'
export default AppTemplateTab

View File

@ -0,0 +1,84 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { memo, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { LinearProgress } from '@mui/material'
import { useFetch } from 'client/hooks'
import { useAuth } from 'client/features/Auth'
import { useMarketplaceAppApi } from 'client/features/One'
import Tabs from 'client/components/Tabs'
import { camelCase } from 'client/utils'
import TabProvider from 'client/components/Tabs/TabProvider'
import Info from 'client/components/Tabs/MarketplaceApp/Info'
import Template from 'client/components/Tabs/MarketplaceApp/Template'
const getTabComponent = tabName => ({
info: Info,
template: Template
}[tabName])
const MarketplaceAppTabs = memo(({ id }) => {
const { getMarketplaceApp } = useMarketplaceAppApi()
const { data, fetchRequest, loading, error } = useFetch(getMarketplaceApp)
const handleRefetch = () => fetchRequest(id, { reload: true })
const [tabsAvailable, setTabs] = useState(() => [])
const { view, getResourceView } = useAuth()
useEffect(() => {
fetchRequest(id)
}, [id])
useEffect(() => {
const infoTabs = getResourceView('MARKETPLACE-APP')?.['info-tabs'] ?? {}
setTabs(() => Object.entries(infoTabs)
?.filter(([_, { enabled } = {}]) => !!enabled)
?.map(([tabName, tabProps]) => {
const camelName = camelCase(tabName)
const TabContent = getTabComponent(camelName)
return TabContent && {
name: camelName,
renderContent: props => TabContent({ ...props, tabProps })
}
})
?.filter(Boolean))
}, [view])
if ((!data && !error) || loading) {
return <LinearProgress color='secondary' style={{ width: '100%' }} />
}
return (
<TabProvider initialState={{ data, handleRefetch }}>
<Tabs tabs={tabsAvailable} />
</TabProvider>
)
})
MarketplaceAppTabs.propTypes = {
id: PropTypes.string.isRequired
}
MarketplaceAppTabs.displayName = 'MarketplaceAppTabs'
export default MarketplaceAppTabs

View File

@ -15,5 +15,6 @@
* ------------------------------------------------------------------------- */
export const MARKETPLACE_APP_ACTIONS = {
REFRESH: 'refresh',
CREATE_DIALOG: 'create_dialog'
CREATE_DIALOG: 'create_dialog',
RENAME: 'rename'
}

View File

@ -286,6 +286,7 @@ module.exports = {
ID: 'ID',
Name: 'Name',
State: 'State',
Size: 'Size',
Description: 'Description',
RegistrationTime: 'Registration time',
StartTime: 'Start time',
@ -295,6 +296,7 @@ module.exports = {
Type: 'Type',
Data: 'Data',
Validate: 'Validate',
Format: 'Format',
/* permissions */
Permissions: 'Permissions',
@ -578,6 +580,11 @@ module.exports = {
ReservedMemory: 'Allocated Memory',
ReservedCpu: 'Allocated CPU',
/* Marketplace App schema */
/* Marketplace App - general */
Version: 'Version',
AppTemplate: 'App Template',
/* User inputs */
UserInputs: 'User Inputs',
UserInputsConcept: `

View File

@ -13,24 +13,53 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { Container, Box } from '@mui/material'
import { useState, JSXElementConstructor } from 'react'
import { Container, Stack, Chip } from '@mui/material'
import { MarketplaceAppsTable } from 'client/components/Tables'
import MarketplaceAppActions from 'client/components/Tables/MarketplaceApps/actions'
import MarketplaceAppsTabs from 'client/components/Tabs/MarketplaceApp'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
/**
* Displays a Marketplace Apps list.
*
* @returns {JSXElementConstructor} List of Marketplace Apps
*/
function MarketplaceApps () {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const actions = MarketplaceAppActions()
return (
<Box
height={1}
py={2}
overflow='auto'
display='flex'
flexDirection='column'
component={Container}
>
<MarketplaceAppsTable />
</Box>
<Stack height={1} py={2} overflow='auto' component={Container}>
<SplitPane>
<MarketplaceAppsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
/>
{selectedRows?.length > 0 && (
<Stack overflow='auto'>
{selectedRows?.length === 1
? <MarketplaceAppsTabs id={selectedRows[0]?.values.ID} />
: <Stack direction='row' flexWrap='wrap' gap={1} alignItems='center'>
<MultipleTags
limitTags={10}
tags={selectedRows?.map(({ original, id, toggleRowSelected }) => (
<Chip key={id}
variant='text'
label={original?.NAME ?? id}
onDelete={() => toggleRowSelected(false)}
/>
))}
/>
</Stack>
}
</Stack>
)}
</SplitPane>
</Stack>
)
}

View File

@ -35,7 +35,7 @@ export const marketplaceAppService = ({
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data?.MARKETAPP ?? {}
return res?.data?.MARKETPLACEAPP ?? {}
},
/**