diff --git a/src/constants/constants.ts b/src/constants/constants.ts index e65be201..148e8aaf 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -7,3 +7,7 @@ export const CARIBBEAN_GREEN_FILL = '#00D3A9'; export const DEFAULT_STROKE = '#000'; export const DEFAULT_STROKE_WIDTH = '2'; export const CLOUD_URL = 'https://cloud.layer5.io'; +export const PLAYGROUND_MODES = { + DESIGNER: 'design', + VISUALIZER: 'visualize' +} as const; diff --git a/src/custom/CatalogDesignTable/CatalogDesignTable.tsx b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx index 42d9c111..bfe48242 100644 --- a/src/custom/CatalogDesignTable/CatalogDesignTable.tsx +++ b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx @@ -5,6 +5,7 @@ import { PublishIcon } from '../../icons'; import { CHARCOAL, useTheme } from '../../theme'; import { Pattern } from '../CustomCatalog/CustomCard'; import { ErrorBoundary } from '../ErrorBoundary'; +import { ColView } from '../Helpers/ResponsiveColumns/responsive-coulmns.tsx/responsive-column'; import PromptComponent from '../Prompt'; import { PromptRef } from '../Prompt/promt-component'; import ResponsiveDataTable from '../ResponsiveDataTable'; @@ -22,7 +23,7 @@ interface CatalogDesignsTableProps { page: number; setPage: (page: number) => void; columnVisibility: Record; - colViews: Record | undefined; + colViews: ColView[]; handleBulkDeleteModal: (patterns: Pattern[], modalRef: React.RefObject) => void; handleBulkpatternsDataUnpublishModal: ( selected: any, @@ -43,7 +44,7 @@ export const CatalogDesignsTable: React.FC = ({ page = 0, setPage, columnVisibility = {}, - colViews = {}, + colViews = [], handleBulkDeleteModal, handleBulkpatternsDataUnpublishModal }) => { diff --git a/src/custom/CatalogDesignTable/DesignTableColumnConfig.tsx b/src/custom/CatalogDesignTable/DesignTableColumnConfig.tsx new file mode 100644 index 00000000..af3ebe5a --- /dev/null +++ b/src/custom/CatalogDesignTable/DesignTableColumnConfig.tsx @@ -0,0 +1,221 @@ +import { MUIDataTableColumn, MUIDataTableMeta } from 'mui-datatables'; +import { PLAYGROUND_MODES } from '../../constants/constants'; +import { ChainIcon, CopyIcon, KanvasIcon, PublishIcon } from '../../icons'; +import Download from '../../icons/Download/Download'; +import { CHARCOAL } from '../../theme'; +import { downloadYaml, slugify } from '../CatalogDetail/helper'; +import { RESOURCE_TYPES } from '../CatalogDetail/types'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { ConditionalTooltip } from '../Helpers/CondtionalTooltip'; +import { ColView } from '../Helpers/ResponsiveColumns/responsive-coulmns.tsx'; +import { DataTableEllipsisMenu } from '../ResponsiveDataTable'; +import AuthorCell from './AuthorCell'; +import { getColumnValue } from './helper'; +import { L5DeleteIcon, NameDiv } from './style'; + +interface TableMeta extends MUIDataTableMeta { + rowIndex: number; + tableData: Pattern[]; +} + +interface ColumnConfigProps { + handleDeleteModal: (data: Pattern) => () => void; + handlePublishModal: (data: Pattern) => void; + handleUnpublishModal: (data: Pattern) => () => void; + handleCopyUrl: (type: string, name: string, id: string) => void; + handleClone: (name: string, id: string) => void; + handleShowDetails: (designId: string, designName: string) => void; + isDownloadDisabled: boolean; + isCopyLinkDisabled: boolean; + isDeleteDisabled: boolean; + isPublishDisabled: boolean; + isUnpublishDisabled: boolean; +} + +export const colViews: ColView[] = [ + ['id', 'na'], + ['name', 'xs'], + ['first_name', 'xs'], + ['created_at', 'na'], + ['updated_at', 'l'], + ['visibility', 'l'], + ['actions', 'xs'] +]; + +export const createDesignsColumnsConfig = ({ + handleDeleteModal, + handlePublishModal, + handleUnpublishModal, + handleCopyUrl, + handleClone, + handleShowDetails, + isUnpublishDisabled, + isCopyLinkDisabled, + isDeleteDisabled, + isPublishDisabled, + isDownloadDisabled +}: ColumnConfigProps): MUIDataTableColumn[] => { + return [ + { + name: 'id', + label: 'ID', + options: { + filter: false, + customBodyRender: (value: string) => + } + }, + { + name: 'name', + label: 'Pattern Name', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (value: string, tableMeta: MUIDataTableMeta) => { + const designId = (tableMeta as TableMeta).tableData[tableMeta.rowIndex]?.id ?? ''; + const designName = (tableMeta as TableMeta).tableData[tableMeta.rowIndex]?.name ?? ''; + + return handleShowDetails(designId, designName)}>{value}; + } + } + }, + { + name: 'first_name', + label: 'Author', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (_, tableMeta: MUIDataTableMeta) => { + const firstName = getColumnValue(tableMeta as TableMeta, 'first_name'); + const lastName = getColumnValue(tableMeta as TableMeta, 'last_name'); + const avatar_url = getColumnValue(tableMeta as TableMeta, 'avatar_url'); + const user_id = getColumnValue(tableMeta as TableMeta, 'user_id'); + + return ( + + ); + } + } + }, + { + name: 'created_at', + label: 'Created At', + options: { + filter: false, + sort: true, + searchable: true, + setCellHeaderProps: () => { + return { align: 'center' }; + } + } + }, + { + name: 'updated_at', + label: 'Updated At', + options: { + filter: false, + sort: true, + searchable: true, + setCellHeaderProps: () => { + return { align: 'center' }; + } + } + }, + { + name: 'visibility', + label: 'Visibility', + options: { + filter: false, + sort: false, + searchable: true + } + }, + { + name: 'actions', + label: 'Actions', + options: { + filter: false, + sort: false, + searchable: false, + setCellHeaderProps: () => ({ align: 'center' as const }), + setCellProps: () => ({ align: 'center' as const }), + customBodyRender: function CustomBody(_, tableMeta: MUIDataTableMeta) { + const rowIndex = (tableMeta as TableMeta).rowIndex; + const rowData = (tableMeta as TableMeta).tableData[rowIndex]; + + const actionsList = [ + { + title: 'Download', + onClick: () => downloadYaml(rowData?.pattern_file, rowData?.name), + disabled: isDownloadDisabled, + icon: + }, + { + title: 'Copy Link', + disabled: rowData.visibility === 'private' || isCopyLinkDisabled, + onClick: () => { + handleCopyUrl(RESOURCE_TYPES.DESIGNS, rowData?.name, rowData?.id); + }, + icon: + }, + { + title: 'Open in playground', + onClick: () => { + window.open( + `https://playground.meshery.io/extension/meshmap?mode=${ + PLAYGROUND_MODES.DESIGNER + }&type=${RESOURCE_TYPES.DESIGNS}&id=${rowData?.id}&name=${slugify( + rowData?.name + )}`, + '_blank' + ); + }, + icon: + }, + { + title: 'Delete', + disabled: isDeleteDisabled, + onClick: () => handleDeleteModal(rowData)(), + icon: + } + ]; + + const publishAction = { + title: 'Publish', + disabled: isPublishDisabled, + onClick: () => handlePublishModal(rowData), + icon: + }; + + const unpublishAction = { + title: 'Unpublish', + onClick: () => handleUnpublishModal(rowData)(), + disabled: isUnpublishDisabled, + icon: + }; + + const cloneAction = { + title: 'Clone', + onClick: () => handleClone(rowData?.name, rowData?.id), + icon: + }; + + if (rowData.visibility === 'published') { + actionsList.splice(0, 0, cloneAction); + actionsList.splice(2, 0, unpublishAction); + } else { + actionsList.splice(1, 0, publishAction); + } + + return ; + } + } + } + ]; +}; diff --git a/src/custom/CatalogDesignTable/columnConfig.tsx b/src/custom/CatalogDesignTable/columnConfig.tsx index 549fd081..bcdf1c45 100644 --- a/src/custom/CatalogDesignTable/columnConfig.tsx +++ b/src/custom/CatalogDesignTable/columnConfig.tsx @@ -17,12 +17,12 @@ import { downloadFilter, downloadYaml } from '../CatalogDetail/helper'; import { RESOURCE_TYPES } from '../CatalogDetail/types'; import { Pattern } from '../CustomCatalog/CustomCard'; import { ConditionalTooltip } from '../Helpers/CondtionalTooltip'; +import { ColView } from '../Helpers/ResponsiveColumns/responsive-coulmns.tsx/responsive-column'; import { DataTableEllipsisMenu } from '../ResponsiveDataTable'; import AuthorCell from './AuthorCell'; +import { getColumnValue } from './helper'; import { NameDiv } from './style'; -export type ColView = [string, 'na' | 'xs' | 'l']; - export const colViews: ColView[] = [ ['id', 'na'], ['name', 'xs'], @@ -83,12 +83,6 @@ export const createDesignColumns = ({ showOpenPlayground }: ColumnConfigProps): MUIDataTableColumn[] => { const cleanedType = type?.replace('my-', '').replace(/s$/, ''); - const getColumnValue = (tableMeta: MUIDataTableMeta, targetColumn: string): any => { - //@ts-ignore - const rowData = tableMeta.tableData[tableMeta.rowIndex] as Pattern; - return (rowData as any)[targetColumn] || ''; - }; - return [ { name: 'id', diff --git a/src/custom/CatalogDesignTable/helper.tsx b/src/custom/CatalogDesignTable/helper.tsx new file mode 100644 index 00000000..a80b784b --- /dev/null +++ b/src/custom/CatalogDesignTable/helper.tsx @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { MUIDataTableMeta } from 'mui-datatables'; + +export const getColumnValue = (tableMeta: MUIDataTableMeta, targetColumn: string): any => { + //@ts-ignore + const rowData = tableMeta.tableData[tableMeta.rowIndex] as Pattern; + return (rowData as any)[targetColumn] || ''; +}; diff --git a/src/custom/CatalogDesignTable/index.ts b/src/custom/CatalogDesignTable/index.ts index 98c1393f..b1f9a4aa 100644 --- a/src/custom/CatalogDesignTable/index.ts +++ b/src/custom/CatalogDesignTable/index.ts @@ -1,6 +1,17 @@ import AuthorCell from './AuthorCell'; import CatalogDesignsTable from './CatalogDesignTable'; import { colViews, createDesignColumns } from './columnConfig'; +import { + createDesignsColumnsConfig, + colViews as designColumnsColViews +} from './DesignTableColumnConfig'; export { TableVisibilityControl } from './TableVisibilityControl'; export { ViewSwitch } from './ViewSwitch'; -export { AuthorCell, CatalogDesignsTable, colViews, createDesignColumns }; +export { + AuthorCell, + CatalogDesignsTable, + colViews, + createDesignColumns, + createDesignsColumnsConfig, + designColumnsColViews +}; diff --git a/src/custom/CatalogDesignTable/style.tsx b/src/custom/CatalogDesignTable/style.tsx index 502129fe..2e01e64b 100644 --- a/src/custom/CatalogDesignTable/style.tsx +++ b/src/custom/CatalogDesignTable/style.tsx @@ -1,4 +1,7 @@ +import DeleteIcon from '@mui/icons-material/Delete'; import { styled } from '@mui/material'; +import { buttonDisabled } from '../../theme'; +import { HOVER_DELETE } from '../../theme/colors/colors'; export const NameDiv = styled('div')({ cursor: 'pointer', @@ -9,3 +12,25 @@ export const NameDiv = styled('div')({ textDecoration: 'underline' } }); + +interface DeleteIconProps { + disabled?: boolean; + bulk?: boolean; +} + +export const L5DeleteIcon = styled(DeleteIcon)(({ disabled, bulk, theme }) => ({ + color: disabled ? theme.palette.icon.disabled : theme.palette.text.secondary, + cursor: disabled ? 'not-allowed' : 'pointer', + width: bulk ? '32' : '28.8', + height: bulk ? '32' : '28.8', + '&:hover': { + color: disabled ? buttonDisabled : HOVER_DELETE, + '& svg': { + color: disabled ? buttonDisabled : HOVER_DELETE + } + }, + '& svg': { + color: theme.palette.error.main, + cursor: disabled ? 'not-allowed' : 'pointer' + } +})); diff --git a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx index 7541891d..c50eb4bd 100644 --- a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx +++ b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx @@ -1,3 +1,4 @@ +import { MUIDataTableColumn } from 'mui-datatables'; import React from 'react'; import { Box } from '../../base/Box'; import { Card } from '../../base/Card'; @@ -10,7 +11,7 @@ import PopperListener from '../PopperListener'; import TooltipIcon from '../TooltipIcon'; export interface CustomColumnVisibilityControlProps { - columns: CustomColumn[]; + columns: MUIDataTableColumn[]; customToolsProps: { columnVisibility: Record; setColumnVisibility: React.Dispatch>>; diff --git a/src/custom/PerformersSection/PerformersSection.tsx b/src/custom/PerformersSection/PerformersSection.tsx index 2fdcc1d8..53b21f83 100644 --- a/src/custom/PerformersSection/PerformersSection.tsx +++ b/src/custom/PerformersSection/PerformersSection.tsx @@ -184,7 +184,7 @@ interface PageArgs { [key: string]: any; } -const withDefaultPageArgs = (args: PageArgs = {}): PageArgs => ({ +export const withDefaultPageArgs = (args: PageArgs = {}): PageArgs => ({ search: args.search ?? '', order: args.order ?? '', pagesize: args.pagesize ?? 0, diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index de09a30b..a37a2a4e 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -4,6 +4,7 @@ import React, { useCallback } from 'react'; import { Checkbox, Collapse, ListItemIcon, ListItemText, Menu, MenuItem } from '../base'; import { ShareIcon } from '../icons'; import { EllipsisIcon } from '../icons/Ellipsis'; +import { ColView } from './Helpers/ResponsiveColumns/responsive-coulmns.tsx'; import TooltipIcon from './TooltipIcon'; export const IconWrapper = styled('div')<{ disabled?: boolean }>(({ disabled = false }) => ({ @@ -283,7 +284,7 @@ export interface ResponsiveDataTableProps { updateCols?: ((columns: Column[]) => void) | undefined; columnVisibility: Record | undefined; theme?: object; - colViews?: Record | undefined; + colViews?: ColView[]; rowsPerPageOptions?: number[] | undefined; backgroundColor?: string; } diff --git a/src/custom/TransferModal/TransferList/TransferList.tsx b/src/custom/TransferModal/TransferList/TransferList.tsx index 8fe1d302..40098fee 100644 --- a/src/custom/TransferModal/TransferList/TransferList.tsx +++ b/src/custom/TransferModal/TransferList/TransferList.tsx @@ -46,7 +46,7 @@ export interface TransferListProps { leftPermission: boolean; } -interface ListItemType { +export interface ListItemType { id: number; name: string; kind: string | undefined; diff --git a/src/custom/Workspaces/AssignmentModal.tsx b/src/custom/Workspaces/AssignmentModal.tsx new file mode 100644 index 00000000..e9933125 --- /dev/null +++ b/src/custom/Workspaces/AssignmentModal.tsx @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Modal, ModalBody, ModalButtonPrimary, ModalButtonSecondary, ModalFooter } from '../Modal'; +import { TransferList } from '../TransferModal/TransferList'; +import { ModalActionDiv } from './styles'; + +interface AssignmentModalProps { + open: boolean; + onClose: (e?: React.MouseEvent) => void; + title: string; + headerIcon: JSX.Element; + name: string; + assignableData: any[]; + handleAssignedData: (data: any) => void; + originalAssignedData: any[]; + emptyStateIcon: JSX.Element; + handleAssignablePage: () => void; + handleAssignedPage: () => void; + originalLeftCount: number; + originalRightCount: number; + onAssign: () => void; + disableTransfer: boolean; + isAssignDisabled: boolean; + isRemoveDisabled: boolean; + helpText: string; +} + +const AssignmentModal: React.FC = ({ + open, + onClose, + title, + headerIcon, + name, + assignableData, + handleAssignedData, + originalAssignedData, + emptyStateIcon, + handleAssignablePage, + handleAssignedPage, + originalLeftCount, + originalRightCount, + onAssign, + disableTransfer, + isAssignDisabled, + isRemoveDisabled, + helpText +}) => { + return ( + + + + + + + Cancel + + Save + + + + + ); +}; + +export default AssignmentModal; diff --git a/src/custom/Workspaces/DesignTable.tsx b/src/custom/Workspaces/DesignTable.tsx new file mode 100644 index 00000000..7d60636b --- /dev/null +++ b/src/custom/Workspaces/DesignTable.tsx @@ -0,0 +1,274 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import _ from 'lodash'; +import React, { useEffect, useRef, useState } from 'react'; +import { Accordion, AccordionDetails, AccordionSummary, Typography } from '../../base'; +import { DesignIcon } from '../../icons'; +import { publishCatalogItemSchema } from '../../schemas'; +import { SistentThemeProvider } from '../../theme'; +import { + CatalogDesignsTable, + createDesignsColumnsConfig, + designColumnsColViews +} from '../CatalogDesignTable'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { CustomColumnVisibilityControl } from '../CustomColumnVisibilityControl'; +import { useWindowDimensions } from '../Helpers/Dimension'; +import { updateVisibleColumns } from '../Helpers/ResponsiveColumns/responsive-coulmns.tsx/responsive-column'; +import PromptComponent from '../Prompt'; +import AssignmentModal from './AssignmentModal'; +import EditButton from './EditButton'; +import useDesignAssignment from './hooks/useDesignAssignment'; +import { TableHeader, TableRightActionHeader } from './styles'; +import { ColumnVisibility } from './types'; + +export interface DesignTableProps { + workspaceId: string; + workspaceName: string; + designsOfWorkspace: any; + meshModelModelsData: any; + useGetWorkspaceDesignsQuery: any; + useAssignDesignToWorkspaceMutation: any; + useUnassignDesignFromWorkspaceMutation: any; + handleCopyUrl: (type: string, name: string, id: string) => void; + handleClone: (name: string, id: string) => void; + handleWorkspaceDesignDeleteModal: (designId: string, workspaceId: string) => void; + handleBulkWorkspaceDesignDeleteModal: ( + designs: Pattern[], + modalRef: React.RefObject, + workspaceName: string, + workspaceId: string + ) => void; + handlePublish: (publishModal: PublishModalState, data: any) => void; + publishModalHandler: any; + handleUnpublishModal: (design: Pattern, modalRef: React.RefObject) => void; + handleBulkUnpublishModal: ( + selected: any, + designs: Pattern[], + modalRef: React.RefObject + ) => void; + handleShowDetails: (designId: string, designName: string) => void; + isDownloadDisabled: boolean; + isCopyLinkDisabled: boolean; + isDeleteDisabled: boolean; + isPublishDisabled: boolean; + isUnpublishDisabled: boolean; + GenericRJSFModal: any; + isAssignDisabled: boolean; + isRemoveDisabled: boolean; +} + +export interface PublishModalState { + open: boolean; + pattern: Partial; +} + +export interface TableColumn { + name: string; + label: string; + [key: string]: any; +} + +const DesignTable: React.FC = ({ + workspaceId, + workspaceName, + designsOfWorkspace, + meshModelModelsData, + handleBulkUnpublishModal, + handleBulkWorkspaceDesignDeleteModal, + handleClone, + handleCopyUrl, + handlePublish, + handleShowDetails, + handleUnpublishModal, + handleWorkspaceDesignDeleteModal, + publishModalHandler, + isCopyLinkDisabled, + isDeleteDisabled, + isDownloadDisabled, + isPublishDisabled, + isUnpublishDisabled, + useAssignDesignToWorkspaceMutation, + useUnassignDesignFromWorkspaceMutation, + GenericRJSFModal, + isAssignDisabled, + isRemoveDisabled, + useGetWorkspaceDesignsQuery +}) => { + const [publishModal, setPublishModal] = useState({ + open: false, + pattern: {} + }); + const modalRef = useRef(null); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [sortOrder, setSortOrder] = useState(''); + + const handlePublishModal = (pattern: Pattern): void => { + const result = publishModalHandler(pattern); + setPublishModal({ + open: true, + pattern: result + }); + }; + + const columns = createDesignsColumnsConfig({ + handleDeleteModal: (design) => () => handleWorkspaceDesignDeleteModal(design.id, workspaceId), + handlePublishModal, + handleUnpublishModal: (design) => () => handleUnpublishModal(design, modalRef), + handleCopyUrl, + handleClone, + handleShowDetails, + isDownloadDisabled, + isCopyLinkDisabled, + isDeleteDisabled, + isPublishDisabled, + isUnpublishDisabled + }); + + const [publishSchema, setPublishSchema] = useState<{ + rjsfSchema: any; + uiSchema: any; + }>({ + rjsfSchema: {}, + uiSchema: {} + }); + + const { width } = useWindowDimensions(); + const [columnVisibility, setColumnVisibility] = useState(() => { + const showCols = updateVisibleColumns(designColumnsColViews, width); + const initialVisibility: ColumnVisibility = {}; + columns.forEach((col) => { + initialVisibility[col.name] = showCols[col.name]; + }); + return initialVisibility; + }); + + const [expanded, setExpanded] = useState(true); + const handleAccordionChange = () => { + setExpanded(!expanded); + }; + + useEffect(() => { + const fetchSchema = async () => { + const modelNames = _.uniq( + meshModelModelsData?.models?.map((model: any) => model.display_name) + ); + const modifiedSchema = _.set( + _.cloneDeep(publishCatalogItemSchema), + 'properties.compatibility.items.enum', + modelNames + ); + setPublishSchema({ + rjsfSchema: modifiedSchema, + uiSchema: publishCatalogItemSchema + }); + }; + fetchSchema(); + }, [meshModelModelsData]); + + const { + disableTransferButton, + assignModal, + handleAssignModalClose, + handleAssignModal, + assignedItems, + data, + workspaceData, + handleAssignablePage, + handleAssignedPage, + handleAssign, + handleAssignData + } = useDesignAssignment({ + workspaceId, + useAssignDesignToWorkspaceMutation, + useUnassignDesignFromWorkspaceMutation, + useGetDesignsOfWorkspaceQuery: useGetWorkspaceDesignsQuery + }); + + const tableHeaderContent = ( + + + Assigned Designs + + + + + + + ); + + return ( + + + } + sx={{ + backgroundColor: 'background.paper' + }} + > + {tableHeaderContent} + + + + handleBulkWorkspaceDesignDeleteModal(designs, modalRef, workspaceName, workspaceId) + } + filter={'my-designs'} + /> + + + } + name="Designs" + assignableData={data} + handleAssignedData={handleAssignData} + originalAssignedData={workspaceData} + emptyStateIcon={} + handleAssignablePage={handleAssignablePage} + handleAssignedPage={handleAssignedPage} + originalLeftCount={data?.length} + originalRightCount={assignedItems?.length} + onAssign={handleAssign} + disableTransfer={disableTransferButton} + helpText={`Assign Designs to ${workspaceName}`} + isAssignDisabled={isAssignDisabled} + isRemoveDisabled={isRemoveDisabled} + /> + setPublishModal({ open: false, pattern: {} })} + schema={publishSchema?.rjsfSchema} + uiSchema={publishSchema?.uiSchema} + handleSubmit={(data: any) => handlePublish(publishModal, data)} + title={`Publish ${publishModal?.pattern?.name}`} + buttonTitle="Publish" + /> + + + ); +}; + +export default DesignTable; diff --git a/src/custom/Workspaces/EditButton.tsx b/src/custom/Workspaces/EditButton.tsx new file mode 100644 index 00000000..7486ae15 --- /dev/null +++ b/src/custom/Workspaces/EditButton.tsx @@ -0,0 +1,24 @@ +import { IconButton } from '@mui/material'; +import React from 'react'; +import { CustomTooltip } from '../CustomTooltip'; +import { L5EditIcon } from './styles'; + +interface EditButtonProps { + onClick: (e: React.MouseEvent) => void; + disabled?: boolean; + title?: string; +} + +const EditButton: React.FC = ({ onClick, disabled, title = 'Edit' }) => { + return ( + +
+ + + +
+
+ ); +}; + +export default EditButton; diff --git a/src/custom/Workspaces/EnvironmentTable.tsx b/src/custom/Workspaces/EnvironmentTable.tsx new file mode 100644 index 00000000..08279576 --- /dev/null +++ b/src/custom/Workspaces/EnvironmentTable.tsx @@ -0,0 +1,300 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { MUIDataTableMeta } from 'mui-datatables'; +import React, { useState } from 'react'; +import { Accordion, AccordionDetails, AccordionSummary, Typography } from '../../base'; +import { DeleteIcon, EnvironmentIcon } from '../../icons'; +import { CHARCOAL, SistentThemeProvider } from '../../theme'; +import { CustomColumnVisibilityControl } from '../CustomColumnVisibilityControl'; +import { CustomTooltip } from '../CustomTooltip'; +import { ConditionalTooltip } from '../Helpers/CondtionalTooltip'; +import { useWindowDimensions } from '../Helpers/Dimension'; +import { + ColView, + updateVisibleColumns +} from '../Helpers/ResponsiveColumns/responsive-coulmns.tsx/responsive-column'; +import ResponsiveDataTable, { IconWrapper } from '../ResponsiveDataTable'; +import AssignmentModal from './AssignmentModal'; +import EditButton from './EditButton'; +import TooltipIcon from './TooltipIcon'; +import useEnvironmentAssignment from './hooks/useEnvironmentAssignment'; +import { CellStyle, CustomBodyRenderStyle, TableHeader, TableRightActionHeader } from './styles'; +import { ColumnVisibility } from './types'; + +interface EnvironmentTableProps { + workspaceId: string; + workspaceName: string; + useGetEnvironmentsOfWorkspaceQuery: any; + useUnassignEnvironmentFromWorkspaceMutation: any; + useAssignEnvironmentToWorkspaceMutation: any; + isRemoveDisabled: boolean; + isAssignDisabled: boolean; +} + +const colViews: ColView[] = [ + ['id', 'na'], + ['name', 'xs'], + ['description', 'm'], + ['organization_id', 'l'], + ['created_at', 'na'], + ['updated_at', 'xl'], + ['actions', 'xs'] +]; + +export const ResizableDescriptionCell = ({ value }: { value: string }) => ( +
+ + + + {value} + + + +
+); + +const EnvironmentTable: React.FC = ({ + workspaceId, + workspaceName, + isRemoveDisabled, + useGetEnvironmentsOfWorkspaceQuery, + useUnassignEnvironmentFromWorkspaceMutation, + useAssignEnvironmentToWorkspaceMutation, + isAssignDisabled +}) => { + const [expanded, setExpanded] = useState(true); + const handleAccordionChange = () => { + setExpanded(!expanded); + }; + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [sortOrder, setSortOrder] = useState(''); + const { data: environmentsOfWorkspace } = useGetEnvironmentsOfWorkspaceQuery({ + workspaceId, + page: page, + pageSize: pageSize, + order: sortOrder + }); + const { width } = useWindowDimensions(); + const [unassignEnvironmentFromWorkspace] = useUnassignEnvironmentFromWorkspaceMutation(); + const columns: any[] = [ + { + name: 'id', + label: 'ID', + options: { + filter: false, + customBodyRender: (value: string) => + } + }, + { + name: 'name', + label: 'Name', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (value: string) => + } + }, + { + name: 'organization_id', + label: 'Organization ID', + options: { + filter: false, + sort: false, + searchable: false + } + }, + + { + name: 'description', + label: 'Description', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (value: string) => + } + }, + { + name: 'created_at', + label: 'Created At', + options: { + filter: false, + sort: true, + searchable: true, + setCellHeaderProps: () => { + return { align: 'center' }; + } + } + }, + { + name: 'updated_at', + label: 'Updated At', + options: { + filter: false, + sort: true, + searchable: true, + setCellHeaderProps: () => { + return { align: 'center' }; + } + } + }, + { + name: 'actions', + label: 'Actions', + options: { + filter: false, + sort: false, + searchable: false, + customBodyRender: (_: any, tableMeta: MUIDataTableMeta) => ( + + { + !isRemoveDisabled && + unassignEnvironmentFromWorkspace({ + workspaceId, + environmentId: tableMeta.rowData[0] + }); + }} + iconType="delete" + > + + + + ) + } + } + ]; + + const environmentAssignment = useEnvironmentAssignment({ + workspaceId, + useGetEnvironmentsOfWorkspaceQuery, + useUnassignEnvironmentFromWorkspaceMutation, + useAssignEnvironmentToWorkspaceMutation + }); + + const [columnVisibility, setColumnVisibility] = useState(() => { + const showCols = updateVisibleColumns(colViews, width); + const initialVisibility: ColumnVisibility = {}; + columns.forEach((col) => { + initialVisibility[col.name] = showCols[col.name]; + }); + return initialVisibility; + }); + + const options = { + filter: false, + responsive: 'standard', + selectableRows: 'none', + count: environmentsOfWorkspace?.total_count, + rowsPerPage: pageSize, + page, + elevation: 0, + serverSide: true, + onTableChange: (action: string, tableState: any) => { + const sortInfo = tableState.announceText ? tableState.announceText.split(' : ') : []; + let order = ''; + if (tableState.activeColumn) { + order = `${columns[tableState.activeColumn].name} desc`; + } + + switch (action) { + case 'changePage': + setPage(tableState.page); + break; + case 'changeRowsPerPage': + setPageSize(tableState.rowsPerPage); + break; + case 'sort': + if (sortInfo.length == 2) { + if (sortInfo[1] === 'ascending') { + order = `${columns[tableState.activeColumn].name} asc`; + } else { + order = `${columns[tableState.activeColumn].name} desc`; + } + } + if (order !== sortOrder) { + setSortOrder(order); + } + break; + } + } + }; + const [tableCols, updateCols] = useState(columns); + + return ( + + + } + sx={{ + backgroundColor: 'background.paper' + }} + > + + + Assigned Environments + + + + + + + + + + + + + } + name="Environments" + assignableData={environmentAssignment.data} + handleAssignedData={environmentAssignment.handleAssignData} + originalAssignedData={environmentAssignment.workspaceData} + emptyStateIcon={} + handleAssignablePage={environmentAssignment.handleAssignablePage} + handleAssignedPage={environmentAssignment.handleAssignedPage} + originalLeftCount={environmentAssignment.data?.length || 0} + originalRightCount={environmentsOfWorkspace?.total_count || 0} + onAssign={environmentAssignment.handleAssign} + disableTransfer={environmentAssignment.disableTransferButton} + helpText={`Assign Environments to ${workspaceName}`} + isAssignDisabled={!isAssignDisabled} + isRemoveDisabled={!isRemoveDisabled} + /> + + ); +}; + +export default EnvironmentTable; diff --git a/src/custom/Workspaces/TooltipIcon.tsx b/src/custom/Workspaces/TooltipIcon.tsx new file mode 100644 index 00000000..6b387b9a --- /dev/null +++ b/src/custom/Workspaces/TooltipIcon.tsx @@ -0,0 +1,54 @@ +import { IconButton } from '@mui/material'; +import { useTheme } from '../../theme'; +import { CustomTooltip } from '../CustomTooltip'; +import { IconWrapper } from '../ResponsiveDataTable'; + +interface TooltipIconProps { + children: React.ReactNode; + onClick: (event: React.MouseEvent) => void; + title: string; + iconType: string; + id: string; + style?: React.CSSProperties; + placement?: 'bottom' | 'top' | 'left' | 'right'; + disabled?: boolean; +} + +const TooltipIcon: React.FC = ({ + children, + onClick, + title, + iconType, + id, + style, + placement, + disabled = false +}) => { + const theme = useTheme(); + return ( + + + + {children} + + + + ); +}; + +export default TooltipIcon; diff --git a/src/custom/Workspaces/hooks/useDesignAssignment.tsx b/src/custom/Workspaces/hooks/useDesignAssignment.tsx new file mode 100644 index 00000000..9b9d4511 --- /dev/null +++ b/src/custom/Workspaces/hooks/useDesignAssignment.tsx @@ -0,0 +1,150 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useState } from 'react'; +import { Pattern } from '../../CustomCatalog/CustomCard'; +import { withDefaultPageArgs } from '../../PerformersSection/PerformersSection'; +import { AssignmentHookResult } from '../types'; + +interface AddedAndRemovedDesigns { + addedDesignsIds: string[]; + removedDesignsIds: string[]; +} + +interface useDesignAssignmentProps { + workspaceId: string; + useGetDesignsOfWorkspaceQuery: any; + useAssignDesignToWorkspaceMutation: any; + useUnassignDesignFromWorkspaceMutation: any; +} + +const useDesignAssignment = ({ + workspaceId, + useGetDesignsOfWorkspaceQuery, + useAssignDesignToWorkspaceMutation, + useUnassignDesignFromWorkspaceMutation +}: useDesignAssignmentProps): AssignmentHookResult => { + const [designsPage, setDesignsPage] = useState(0); + const [designsData, setDesignsData] = useState([]); + const designsPageSize = 25; + const [designsOfWorkspacePage, setDesignsOfWorkspacePage] = useState(0); + const [workspaceDesignsData, setWorkspaceDesignsData] = useState([]); + const [assignDesignModal, setAssignDesignModal] = useState(false); + const [skipDesigns, setSkipDesigns] = useState(true); + const [disableTransferButton, setDisableTransferButton] = useState(true); + const [assignedDesigns, setAssignedDesigns] = useState([]); + + const { data: designs } = useGetDesignsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: designsPage, + pagesize: designsPageSize, + filter: '{"assigned":false}' + }), + { + skip: skipDesigns + } + ); + + const { data: designsOfWorkspace } = useGetDesignsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: designsOfWorkspacePage, + pagesize: designsPageSize + }), + { + skip: skipDesigns + } + ); + + const [assignDesignToWorkspace] = useAssignDesignToWorkspaceMutation(); + const [unassignDesignFromWorkspace] = useUnassignDesignFromWorkspaceMutation(); + + useEffect(() => { + const designsDataRtk = designs?.designs ? designs.designs : []; + setDesignsData((prevData) => [...prevData, ...designsDataRtk]); + }, [designs]); + + useEffect(() => { + const designsOfWorkspaceDataRtk = designsOfWorkspace?.designs ? designsOfWorkspace.designs : []; + setWorkspaceDesignsData((prevData) => [...prevData, ...designsOfWorkspaceDataRtk]); + }, [designsOfWorkspace]); + + const handleAssignDesignModal = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + setAssignDesignModal(true); + setSkipDesigns(false); + }; + + const handleAssignDesignModalClose = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + setAssignDesignModal(false); + setSkipDesigns(true); + }; + + const handleAssignablePageDesign = (): void => { + const pagesCount = Math.ceil(Number(designs?.total_count) / designsPageSize); + if (designsPage < pagesCount - 1) { + setDesignsPage((prevDesignsPage) => prevDesignsPage + 1); + } + }; + + const handleAssignedPageDesign = (): void => { + const pagesCount = Math.ceil(Number(designsOfWorkspace?.total_count) / designsPageSize); + if (designsOfWorkspacePage < pagesCount - 1) { + setDesignsOfWorkspacePage((prevPage) => prevPage + 1); + } + }; + + const getAddedAndRemovedDesigns = (allAssignedDesigns: Pattern[]): AddedAndRemovedDesigns => { + const originalDesignsIds = workspaceDesignsData.map((design) => design.id); + const updatedDesignsIds = allAssignedDesigns.map((design) => design.id); + + const addedDesignsIds = updatedDesignsIds.filter((id) => !originalDesignsIds.includes(id)); + const removedDesignsIds = originalDesignsIds.filter((id) => !updatedDesignsIds.includes(id)); + + return { addedDesignsIds, removedDesignsIds }; + }; + + const handleAssignDesigns = async (): Promise => { + const { addedDesignsIds, removedDesignsIds } = getAddedAndRemovedDesigns(assignedDesigns); + + for (const id of addedDesignsIds) { + await assignDesignToWorkspace({ + workspaceId, + designId: id + }).unwrap(); + } + + for (const id of removedDesignsIds) { + await unassignDesignFromWorkspace({ + workspaceId, + designId: id + }).unwrap(); + } + + setDesignsData([]); + setWorkspaceDesignsData([]); + handleAssignDesignModalClose(); + }; + + const handleAssignDesignsData = (updatedAssignedData: Pattern[]): void => { + const { addedDesignsIds, removedDesignsIds } = getAddedAndRemovedDesigns(updatedAssignedData); + setDisableTransferButton(!(addedDesignsIds.length > 0 || removedDesignsIds.length > 0)); + setAssignedDesigns(updatedAssignedData); + }; + + return { + data: designsData, + workspaceData: workspaceDesignsData, + assignModal: assignDesignModal, + handleAssignModal: handleAssignDesignModal, + handleAssignModalClose: handleAssignDesignModalClose, + handleAssignablePage: handleAssignablePageDesign, + handleAssignedPage: handleAssignedPageDesign, + handleAssign: handleAssignDesigns, + handleAssignData: handleAssignDesignsData, + disableTransferButton, + assignedItems: assignedDesigns + }; +}; + +export default useDesignAssignment; diff --git a/src/custom/Workspaces/hooks/useEnvironmentAssignment.tsx b/src/custom/Workspaces/hooks/useEnvironmentAssignment.tsx new file mode 100644 index 00000000..fb346db7 --- /dev/null +++ b/src/custom/Workspaces/hooks/useEnvironmentAssignment.tsx @@ -0,0 +1,156 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useState } from 'react'; +import { withDefaultPageArgs } from '../../PerformersSection/PerformersSection'; +import { AssignmentHookResult, Environment } from '../types'; + +interface UseEnvironmentAssignmentProps { + workspaceId: string; + useGetEnvironmentsOfWorkspaceQuery: any; + useAssignEnvironmentToWorkspaceMutation: any; + useUnassignEnvironmentFromWorkspaceMutation: any; +} + +const useEnvironmentAssignment = ({ + workspaceId, + useGetEnvironmentsOfWorkspaceQuery, + useAssignEnvironmentToWorkspaceMutation, + useUnassignEnvironmentFromWorkspaceMutation +}: UseEnvironmentAssignmentProps): AssignmentHookResult => { + const [environmentsPage, setEnvironmentsPage] = useState(0); + const [environmentsData, setEnvironmentsData] = useState([]); + const environmentsPageSize = 25; + const [environmentsOfWorkspacePage, setEnvironmentsOfWorkspacePage] = useState(0); + const [workspaceEnvironmentsData, setWorkspaceEnvironmentsData] = useState([]); + const [assignEnvironmentModal, setAssignEnvironmentModal] = useState(false); + const [skipEnvironments, setSkipEnvironments] = useState(true); + + const { data: environments } = useGetEnvironmentsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: environmentsPage, + pagesize: environmentsPageSize, + filter: '{"assigned":false}' + }), + { + skip: skipEnvironments + } + ); + + const { data: environmentsOfWorkspace } = useGetEnvironmentsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: environmentsOfWorkspacePage, + pagesize: environmentsPageSize + }), + { + skip: skipEnvironments + } + ); + + const [assignEnvironmentToWorkspace] = useAssignEnvironmentToWorkspaceMutation(); + const [unassignEnvironmentFromWorkspace] = useUnassignEnvironmentFromWorkspaceMutation(); + const [disableTransferButton, setDisableTransferButton] = useState(true); + const [assignedEnvironments, setAssignedEnvironments] = useState([]); + + useEffect(() => { + const environmentsDataRtk = environments?.environments ? environments.environments : []; + setEnvironmentsData((prevData) => [...prevData, ...environmentsDataRtk]); + }, [environments]); + + useEffect(() => { + const environmentsOfWorkspaceDataRtk = environmentsOfWorkspace?.environments + ? environmentsOfWorkspace.environments + : []; + setWorkspaceEnvironmentsData((prevData) => [...prevData, ...environmentsOfWorkspaceDataRtk]); + }, [environmentsOfWorkspace]); + + const handleAssignEnvironmentModal = (e?: React.MouseEvent) => { + e?.stopPropagation(); + setAssignEnvironmentModal(true); + setSkipEnvironments(false); + }; + + const handleAssignEnvironmentModalClose = (e?: React.MouseEvent) => { + e?.stopPropagation(); + setAssignEnvironmentModal(false); + setSkipEnvironments(true); + }; + + const handleAssignablePageEnvironment = () => { + const pagesCount = Math.ceil(Number(environments?.total_count) / environmentsPageSize); + if (environmentsPage < pagesCount - 1) { + setEnvironmentsPage((prevEnvironmentsPage) => prevEnvironmentsPage + 1); + } + }; + + const handleAssignedPageEnvironment = () => { + const pagesCount = Math.ceil( + Number(environmentsOfWorkspace?.total_count) / environmentsPageSize + ); + if (environmentsOfWorkspacePage < pagesCount - 1) { + setEnvironmentsOfWorkspacePage((prevPage) => prevPage + 1); + } + }; + + const handleAssignEnvironments = async () => { + const { addedEnvironmentsIds, removedEnvironmentsIds } = + getAddedAndRemovedEnvironments(assignedEnvironments); + + for (const id of addedEnvironmentsIds) { + await assignEnvironmentToWorkspace({ + workspaceId, + environmentId: id + }).unwrap(); + } + + for (const id of removedEnvironmentsIds) { + await unassignEnvironmentFromWorkspace({ + workspaceId, + environmentId: id + }).unwrap(); + } + + setEnvironmentsData([]); + setWorkspaceEnvironmentsData([]); + handleAssignEnvironmentModalClose(); + }; + + const getAddedAndRemovedEnvironments = (allAssignedEnvironments: Environment[]) => { + const originalEnvironmentsIds = workspaceEnvironmentsData.map((env) => env.id); + const updatedEnvironmentsIds = allAssignedEnvironments.map((env) => env.id); + + const addedEnvironmentsIds = updatedEnvironmentsIds.filter( + (id) => !originalEnvironmentsIds.includes(id) + ); + const removedEnvironmentsIds = originalEnvironmentsIds.filter( + (id) => !updatedEnvironmentsIds.includes(id) + ); + + return { addedEnvironmentsIds, removedEnvironmentsIds }; + }; + + const handleAssignEnvironmentsData = (updatedAssignedData: Environment[]) => { + const { addedEnvironmentsIds, removedEnvironmentsIds } = + getAddedAndRemovedEnvironments(updatedAssignedData); + addedEnvironmentsIds.length > 0 || removedEnvironmentsIds.length > 0 + ? setDisableTransferButton(false) + : setDisableTransferButton(true); + setAssignedEnvironments(updatedAssignedData); + }; + + return { + data: environmentsData, + workspaceData: workspaceEnvironmentsData, + assignModal: assignEnvironmentModal, + handleAssignModal: handleAssignEnvironmentModal, + handleAssignModalClose: handleAssignEnvironmentModalClose, + handleAssignablePage: handleAssignablePageEnvironment, + handleAssignedPage: handleAssignedPageEnvironment, + handleAssign: handleAssignEnvironments, + handleAssignData: handleAssignEnvironmentsData, + disableTransferButton, + assignedItems: assignedEnvironments + }; +}; + +export default useEnvironmentAssignment; diff --git a/src/custom/Workspaces/index.ts b/src/custom/Workspaces/index.ts new file mode 100644 index 00000000..4294ab26 --- /dev/null +++ b/src/custom/Workspaces/index.ts @@ -0,0 +1,4 @@ +import AssignmentModal from './AssignmentModal'; +import DesignTable from './DesignTable'; +import EnvironmentTable from './EnvironmentTable'; +export { AssignmentModal, DesignTable, EnvironmentTable }; diff --git a/src/custom/Workspaces/styles.ts b/src/custom/Workspaces/styles.ts new file mode 100644 index 00000000..92ac5238 --- /dev/null +++ b/src/custom/Workspaces/styles.ts @@ -0,0 +1,65 @@ +import EditIcon from '@mui/icons-material/Edit'; +import { buttonDisabled, styled } from '../../theme'; +import { KEPPEL } from '../../theme/colors/colors'; + +export const ModalActionDiv = styled('div')({ + display: 'flex', + gap: '1rem' +}); + +interface ExtendedEditIconProps { + disabled?: boolean; + bulk?: boolean; + style?: React.CSSProperties; +} + +export const L5EditIcon = styled(EditIcon)( + ({ disabled, bulk, style, theme }) => ({ + color: disabled ? theme.palette.icon.disabled : theme.palette.text.secondary, + cursor: disabled ? 'not-allowed' : 'pointer', + width: bulk ? '32' : '28.8', + height: bulk ? '32' : '28.8', + '&:hover': { + color: disabled ? buttonDisabled : KEPPEL, + '& svg': { + color: disabled ? buttonDisabled : KEPPEL + } + }, + '& svg': { + color: theme.palette.error.main, + cursor: disabled ? 'not-allowed' : 'pointer' + }, + ...style + }) +); + +export const TableHeader = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + width: '100%', + alignItems: 'center' +}); + +export const TableRightActionHeader = styled('div')({ + display: 'flex', + alignItems: 'center', + marginRight: '1rem' +}); + +export const CellStyle = styled('div')({ + boxSizing: 'border-box', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' +}); + +export const CustomBodyRenderStyle = styled('div')({ + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + boxSizing: 'border-box', + display: 'block', + width: '100%' +}); diff --git a/src/custom/Workspaces/types.ts b/src/custom/Workspaces/types.ts new file mode 100644 index 00000000..ad391f76 --- /dev/null +++ b/src/custom/Workspaces/types.ts @@ -0,0 +1,42 @@ +export interface AssignmentHookResult { + data: T[]; + workspaceData: T[]; + assignModal: boolean; + handleAssignModal: (e?: React.MouseEvent) => void; + handleAssignModalClose: (e?: React.MouseEvent) => void; + handleAssignablePage: () => void; + handleAssignedPage: () => void; + handleAssign: () => void; + handleAssignData: (data: T[]) => void; + disableTransferButton: boolean; + assignedItems: T[]; +} + +export interface Workspace { + id: string; + name: string; + description?: string; + created_at: string; + updated_at: string; + deleted_at: { + Valid: boolean; + }; +} + +export interface Environment { + id: string; + name: string; + description?: string; + organization_id: string; + created_at: string; + updated_at: string; +} + +export interface ColumnVisibility { + [key: string]: boolean; +} + +export interface Team { + id: string; + name: string; +} diff --git a/src/custom/index.tsx b/src/custom/index.tsx index ded1e4f5..a2202b9a 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -136,4 +136,5 @@ export type { export * from './CatalogDesignTable'; export * from './CatalogDetail'; export * from './Dialog'; +export * from './Workspaces'; export * from './permissions'; diff --git a/src/theme/colors/colors.ts b/src/theme/colors/colors.ts index 107bc4f3..8823dc58 100644 --- a/src/theme/colors/colors.ts +++ b/src/theme/colors/colors.ts @@ -95,7 +95,7 @@ export const charcoal = { 70: '#B1B9BC', 60: '#8C999E', 50: '#647176', - 40: '#3C494E', + 40: '#3C494F', 30: '#28353A', 20: '#142126', 10: '#000D12'