diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml deleted file mode 100644 index 76dc280a2..000000000 --- a/.github/workflows/checks.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Checks -on: - pull_request: - types: [opened, synchronize] - -jobs: - checks: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [16, 18, 20] - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Setup and install deps - run: | - npm install - - - name: Prettier check - run: | - npm run format:check - - - name: Build - run: | - npm run build - - - name: Test - run: | - npm run test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 4fdbc6f2b..000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Lint check - -on: - push: - branches: - - '*' - paths-ignore: - - 'system/**/*' - - '.github/**/*' - - '*.md' - pull_request: - branches: - - '*' -jobs: - lint: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [16, 18, 20] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - run: npm install - - - name: Run Lint - run: npm run lint diff --git a/.github/workflows/node-checks.yml b/.github/workflows/node-checks.yml new file mode 100644 index 000000000..aad8ab9d9 --- /dev/null +++ b/.github/workflows/node-checks.yml @@ -0,0 +1,44 @@ +name: Node version and Lint Check +on: + pull_request: + types: [opened, synchronize] + push: + branches: + - '*' + paths-ignore: + - 'system/**/*' + - '.github/**/*' + - '*.md' + +jobs: + compatibility-check: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [16, 18, 20] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Dependencies + run: npm install + + - name: Lint Check + run: npm run lint + + - name: Prettier Check + run: npm run format:check + + - name: Build Project + run: npm run build + + - name: Run Tests + run: npm run test + + - name: Log Node.js Version + run: echo "Tested on Node.js version ${{ matrix.node-version }}" diff --git a/package-lock.json b/package-lock.json index 887072551..128b00f71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "billboard.js": "^3.14.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "moment": "^2.30.1", "re-resizable": "^6.10.3", "react-draggable": "^4.4.6", - "moment": "^2.30.1", "react-share": "^5.1.0" }, "devDependencies": { diff --git a/package.json b/package.json index 162bb5a20..103a3ac36 100644 --- a/package.json +++ b/package.json @@ -119,9 +119,9 @@ "billboard.js": "^3.14.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "moment": "^2.30.1", "re-resizable": "^6.10.3", "react-draggable": "^4.4.6", - "moment": "^2.30.1", "react-share": "^5.1.0" } } diff --git a/src/custom/CatalogDesignTable/CatalogDesignTable.tsx b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx index 552420b8e..431f9c24d 100644 --- a/src/custom/CatalogDesignTable/CatalogDesignTable.tsx +++ b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx @@ -28,6 +28,7 @@ interface CatalogDesignsTableProps { rowsPerPageOptions?: number[]; handleBulkDeleteModal: (patterns: Pattern[], modalRef: React.RefObject) => void; setSearch?: (search: string) => void; + tableBackgroundColor?: string; handleBulkpatternsDataUnpublishModal: ( selected: any, patterns: Pattern[], @@ -51,6 +52,7 @@ export const CatalogDesignsTable: React.FC = ({ handleBulkDeleteModal, setSearch, rowsPerPageOptions = [10, 25, 50, 100], + tableBackgroundColor, handleBulkpatternsDataUnpublishModal }) => { const theme = useTheme(); @@ -203,7 +205,9 @@ export const CatalogDesignsTable: React.FC = ({ tableCols={processedColumns} columnVisibility={columnVisibility} backgroundColor={ - theme.palette.mode === 'light' + tableBackgroundColor + ? tableBackgroundColor + : theme.palette.mode === 'light' ? theme.palette.background.default : theme.palette.background.secondary } diff --git a/src/custom/CatalogDesignTable/DesignTableColumnConfig.tsx b/src/custom/CatalogDesignTable/DesignTableColumnConfig.tsx index 17c26b203..b4323e657 100644 --- a/src/custom/CatalogDesignTable/DesignTableColumnConfig.tsx +++ b/src/custom/CatalogDesignTable/DesignTableColumnConfig.tsx @@ -1,15 +1,15 @@ +import { Theme } from '@mui/material'; 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 { downloadPattern, 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 { UserTableAvatarInfo } from '../UsersTable'; import { getColumnValue } from './helper'; import { L5DeleteIcon, NameDiv } from './style'; @@ -25,7 +25,8 @@ interface ColumnConfigProps { handleCopyUrl: (type: string, name: string, id: string) => void; handleClone: (name: string, id: string) => void; handleShowDetails: (designId: string, designName: string) => void; - getDownloadUrl: (id: string) => string; + handleDownload?: (design: Pattern) => void; + getDownloadUrl?: (id: string) => string; isDownloadAllowed: boolean; isCopyLinkAllowed: boolean; isDeleteAllowed: boolean; @@ -34,6 +35,7 @@ interface ColumnConfigProps { // for workspace designs table page only isFromWorkspaceTable?: boolean; isRemoveAllowed?: boolean; + theme?: Theme; } export const colViews: ColView[] = [ @@ -55,12 +57,14 @@ export const createDesignsColumnsConfig = ({ handleClone, handleShowDetails, getDownloadUrl, + handleDownload, isUnpublishAllowed, isCopyLinkAllowed, isDeleteAllowed, isPublishAllowed, isDownloadAllowed, isRemoveAllowed, + theme, isFromWorkspaceTable = false }: ColumnConfigProps): MUIDataTableColumn[] => { return [ @@ -99,13 +103,14 @@ export const createDesignsColumnsConfig = ({ 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'); + const userEmail = getColumnValue(tableMeta as TableMeta, 'email'); return ( - ); } @@ -153,6 +158,17 @@ export const createDesignsColumnsConfig = ({ searchable: false } }, + + { + name: 'email', + label: 'email', + options: { + filter: false, + sort: false, + searchable: false + } + }, + { name: 'actions', label: 'Actions', @@ -165,13 +181,14 @@ export const createDesignsColumnsConfig = ({ customBodyRender: function CustomBody(_, tableMeta: MUIDataTableMeta) { const rowIndex = (tableMeta as TableMeta).rowIndex; const rowData = (tableMeta as TableMeta).tableData[rowIndex]; - const actionsList = [ { title: 'Download', - onClick: () => downloadPattern(rowData.id, rowData.name, getDownloadUrl), + onClick: getDownloadUrl + ? () => downloadPattern(rowData.id, rowData.name, getDownloadUrl) + : () => handleDownload && handleDownload(rowData), disabled: !isDownloadAllowed, - icon: + icon: }, { title: 'Copy Link', @@ -179,7 +196,7 @@ export const createDesignsColumnsConfig = ({ onClick: () => { handleCopyUrl(RESOURCE_TYPES.DESIGN, rowData?.name, rowData?.id); }, - icon: + icon: }, { title: 'Open in playground', @@ -191,7 +208,9 @@ export const createDesignsColumnsConfig = ({ '_blank' ); }, - icon: + icon: ( + + ) }, { title: isFromWorkspaceTable ? 'Remove Design' : 'Delete', @@ -205,20 +224,20 @@ export const createDesignsColumnsConfig = ({ title: 'Publish', disabled: !isPublishAllowed, onClick: () => handlePublishModal(rowData), - icon: + icon: }; const unpublishAction = { title: 'Unpublish', onClick: () => handleUnpublishModal(rowData)(), disabled: !isUnpublishAllowed, - icon: + icon: }; const cloneAction = { title: 'Clone', onClick: () => handleClone(rowData?.name, rowData?.id), - icon: + icon: }; if (rowData.visibility === 'published') { @@ -228,7 +247,7 @@ export const createDesignsColumnsConfig = ({ actionsList.splice(1, 0, publishAction); } - return ; + return ; } } } diff --git a/src/custom/CatalogDesignTable/columnConfig.tsx b/src/custom/CatalogDesignTable/columnConfig.tsx index 06cca4e06..1979510b5 100644 --- a/src/custom/CatalogDesignTable/columnConfig.tsx +++ b/src/custom/CatalogDesignTable/columnConfig.tsx @@ -320,7 +320,7 @@ export const createDesignColumns = ({ }); } //@ts-ignore - return ; + return ; } } } diff --git a/src/custom/CatalogDesignTable/style.tsx b/src/custom/CatalogDesignTable/style.tsx index 2e01e64b9..b38e9c5d5 100644 --- a/src/custom/CatalogDesignTable/style.tsx +++ b/src/custom/CatalogDesignTable/style.tsx @@ -19,7 +19,7 @@ interface DeleteIconProps { } export const L5DeleteIcon = styled(DeleteIcon)(({ disabled, bulk, theme }) => ({ - color: disabled ? theme.palette.icon.disabled : theme.palette.text.secondary, + color: disabled ? theme.palette.icon.disabled : theme.palette.text.default, cursor: disabled ? 'not-allowed' : 'pointer', width: bulk ? '32' : '28.8', height: bulk ? '32' : '28.8', diff --git a/src/custom/CatalogDetail/RelatedDesigns.tsx b/src/custom/CatalogDetail/RelatedDesigns.tsx index 41f92d5e0..00396b508 100644 --- a/src/custom/CatalogDetail/RelatedDesigns.tsx +++ b/src/custom/CatalogDetail/RelatedDesigns.tsx @@ -1,6 +1,6 @@ import { CatalogCardDesignLogo } from '../CustomCatalog'; import CustomCatalogCard, { Pattern } from '../CustomCatalog/CustomCard'; -import { formatToTitleCase } from './helper'; +import { getHeadingText } from './helper'; import { AdditionalContainer, ContentHeading, DesignCardContainer } from './style'; import { UserProfile } from './types'; @@ -41,8 +41,7 @@ const RelatedDesigns: React.FC = ({

- Other published design by {formatToTitleCase(userProfile?.first_name ?? '')}{' '} - {fetchingOrgError ? '' : `under ${organizationName}`} + {getHeadingText({ type, userProfile, organizationName, fetchingOrgError })}

diff --git a/src/custom/CatalogDetail/helper.ts b/src/custom/CatalogDetail/helper.ts index b43477bf9..272347176 100644 --- a/src/custom/CatalogDetail/helper.ts +++ b/src/custom/CatalogDetail/helper.ts @@ -61,3 +61,25 @@ export const formatDate = (date: Date) => { const formattedDate = new Date(date).toLocaleDateString('en-US', options); return formattedDate; }; + +interface HeadingProps { + type: string; + userProfile?: { + first_name?: string; + }; + organizationName?: string; + fetchingOrgError: boolean; +} + +export const getHeadingText = ({ + type, + userProfile, + organizationName, + fetchingOrgError +}: HeadingProps): string => { + const designType = type.toLowerCase() === 'my-designs' ? 'public' : 'published'; + const firstName = formatToTitleCase(userProfile?.first_name ?? ''); + const orgText = fetchingOrgError ? '' : `under ${organizationName}`; + + return `Other ${designType} design by ${firstName} ${orgText}`; +}; diff --git a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx index bb420e38b..26bbc45d0 100644 --- a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx +++ b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx @@ -36,8 +36,13 @@ export function CustomColumnVisibilityControl({ const theme = useTheme(); const handleOpen = (event: React.MouseEvent) => { + event.stopPropagation(); + setOpen((prev) => !prev); + if (anchorEl) { + setAnchorEl(null); + return; + } setAnchorEl(event.currentTarget); - setOpen(true); }; const handleClose = () => { diff --git a/src/custom/FlipCard/FlipCard.tsx b/src/custom/FlipCard/FlipCard.tsx index 7ecceaa05..895964773 100644 --- a/src/custom/FlipCard/FlipCard.tsx +++ b/src/custom/FlipCard/FlipCard.tsx @@ -7,8 +7,17 @@ export type FlipCardProps = { onClick?: () => void; onShow?: () => void; children: [React.ReactNode, React.ReactNode]; + disableFlip?: boolean; + padding?: string; }; +/** + * Helper function to get the front or back child component from the children array + * @param children Array containing exactly two child components + * @param key Index to retrieve (0 for front, 1 for back) + * @throws Error if children is undefined or doesn't contain exactly two components + * @returns The selected child component + */ function GetChild(children: [React.ReactNode, React.ReactNode], key: number) { if (!children) throw Error('FlipCard requires exactly two child components'); if (children.length != 2) throw Error('FlipCard requires exactly two child components'); @@ -42,7 +51,32 @@ const BackContent = styled('div')({ wordBreak: 'break-word' }); -export function FlipCard({ duration = 500, onClick, onShow, children }: FlipCardProps) { +/** + * A card component that provides a flipping animation between two content faces + * + * @component + * @param props.duration - Animation duration in milliseconds (default: 500) + * @param props.onClick - Callback function triggered on card click + * @param props.onShow - Additional callback function triggered when card shows new face + * @param props.children - Array of exactly two child components (front and back) + * @param props.disableFlip - When true, prevents the card from flipping (default: false) + * + * @example + * ```tsx + * + *
Front Content
+ *
Back Content
+ *
+ * ``` + */ +export function FlipCard({ + duration = 500, + onClick, + onShow, + children, + disableFlip = false, + padding +}: FlipCardProps) { const [flipped, setFlipped] = React.useState(false); const [activeBack, setActiveBack] = React.useState(false); @@ -72,6 +106,7 @@ export function FlipCard({ duration = 500, onClick, onShow, children }: FlipCard return ( { + if (disableFlip) return; setFlipped((flipped) => !flipped); onClick && onClick(); onShow && onShow(); @@ -80,7 +115,8 @@ export function FlipCard({ duration = 500, onClick, onShow, children }: FlipCard {!activeBack ? ( diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index f59b55d22..0c281f102 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -48,7 +48,7 @@ export const DataTableEllipsisMenu: React.FC<{ } + icon={} arrow /> diff --git a/src/custom/TeamTable/TeamTable.tsx b/src/custom/TeamTable/TeamTable.tsx index cfa5de4ad..ba8dd6791 100644 --- a/src/custom/TeamTable/TeamTable.tsx +++ b/src/custom/TeamTable/TeamTable.tsx @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Grid, TableCell } from '@mui/material'; +import { TableCell } from '@mui/material'; import { MUIDataTableColumn } from 'mui-datatables'; +import { Grid } from '../../base'; +import { styled, useTheme } from '../../theme'; import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary.js'; import { ColView } from '../Helpers/ResponsiveColumns/responsive-coulmns.tsx/index.js'; import ResponsiveDataTable from '../ResponsiveDataTable.js'; @@ -20,6 +22,14 @@ interface TeamTableProps { useNotificationHandlers: any; useRemoveUserFromTeamMutation: any; } +const StyledGrid = styled(Grid)(({ theme }) => ({ + display: 'grid', + margin: 'auto', + paddingLeft: '0.5rem', + borderRadius: '0.25rem', + width: 'inherit', + gap: theme.spacing(1) +})); const TeamTable: React.FC = ({ teams, @@ -35,6 +45,7 @@ const TeamTable: React.FC = ({ useNotificationHandlers, useRemoveUserFromTeamMutation }) => { + const theme = useTheme(); return ( = ({ - + = ({ useGetUsersForOrgQuery={useGetUsersForOrgQuery} useNotificationHandlers={useNotificationHandlers} useRemoveUserFromTeamMutation={useRemoveUserFromTeamMutation} + theme={theme} /> - + ); } diff --git a/src/custom/TeamTable/TeamTableConfiguration.tsx b/src/custom/TeamTable/TeamTableConfiguration.tsx index 80d584a99..36e8c3759 100644 --- a/src/custom/TeamTable/TeamTableConfiguration.tsx +++ b/src/custom/TeamTable/TeamTableConfiguration.tsx @@ -256,7 +256,7 @@ export default function TeamTableConfiguration({ }} iconType="delete" > - + ) : ( @@ -296,12 +296,6 @@ export default function TeamTableConfiguration({ download: false, elevation: 0, serverSide: true, - tableBody: { - style: { - backgroundColor: '#f3f1f1' - } - }, - viewColumns: false, search: false, rowsExpanded: [ExpandedRowIdx], @@ -392,7 +386,7 @@ export default function TeamTableConfiguration({ return { style: { - backgroundColor: theme.palette.background.paper + backgroundColor: theme.palette.background.constant?.table } }; } diff --git a/src/custom/UsersTable/UserTableAvatarInfo.tsx b/src/custom/UsersTable/UserTableAvatarInfo.tsx new file mode 100644 index 000000000..3f019cea0 --- /dev/null +++ b/src/custom/UsersTable/UserTableAvatarInfo.tsx @@ -0,0 +1,49 @@ +import { Avatar, Box, Grid, Typography } from '../../base'; +import { CLOUD_URL } from '../../constants/constants'; +import { PersonIcon } from '../../icons'; +import { useTheme } from '../../theme'; + +interface UserTableAvatarInfoProps { + userId: string; + userName: string; + userEmail: string; + profileUrl?: string; +} + +const UserTableAvatarInfo: React.FC = ({ + userId, + userName, + userEmail, + profileUrl +}): JSX.Element => { + const theme = useTheme(); + const handleProfileClick = (): void => { + window.open(`${CLOUD_URL}/user/${userId}`); + }; + + return ( + + + + + {profileUrl ? '' : } + + + + + {userName} + + {userEmail} + + + + ); +}; + +export default UserTableAvatarInfo; diff --git a/src/custom/UsersTable/UsersTable.tsx b/src/custom/UsersTable/UsersTable.tsx index ba389b1c4..69458f839 100644 --- a/src/custom/UsersTable/UsersTable.tsx +++ b/src/custom/UsersTable/UsersTable.tsx @@ -1,33 +1,36 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { Theme } from '@mui/material'; import { MUIDataTableColumn, MUIDataTableMeta } from 'mui-datatables'; import { useRef, useState } from 'react'; -import { Avatar, Box, Grid, Tooltip, Typography } from '../../base'; -import { EditIcon, PersonIcon } from '../../icons'; +import { Box, Tooltip } from '../../base'; +import { EditIcon } from '../../icons'; import Github from '../../icons/Github/GithubIcon'; import Google from '../../icons/Google/GoogleIcon'; import LogoutIcon from '../../icons/Logout/LogOutIcon'; -import { CHARCOAL, SistentThemeProvider } from '../../theme'; +import { CHARCOAL, SistentThemeProviderWithoutBaseLine } from '../../theme'; import { useWindowDimensions } from '../Helpers/Dimension'; import { ColView, updateVisibleColumns } from '../Helpers/ResponsiveColumns/responsive-coulmns.tsx/responsive-column'; -import PromptComponent from '../Prompt'; +import PromptComponent, { PROMPT_VARIANTS } from '../Prompt'; import ResponsiveDataTable from '../ResponsiveDataTable'; import { TooltipIcon } from '../TooltipIconButton'; import { parseDeletionTimestamp } from '../Workspaces/helper'; import { TableIconsContainer, TableIconsDisabledContainer } from '../Workspaces/styles'; - +import UserTableAvatarInfo from './UserTableAvatarInfo'; interface ActionButtonsProps { tableMeta: MUIDataTableMeta; isRemoveFromTeamAllowed: boolean; handleRemoveFromTeam: (data: any[]) => () => void; + theme?: Theme; } const ActionButtons: React.FC = ({ tableMeta, handleRemoveFromTeam, - isRemoveFromTeamAllowed + isRemoveFromTeamAllowed, + theme }) => { return (
@@ -39,12 +42,12 @@ const ActionButtons: React.FC = ({ title="Remove user membership from team" iconType="delete" > - + ) : ( - + )}
@@ -58,6 +61,7 @@ interface UsersTableProps { useRemoveUserFromTeamMutation: any; useNotificationHandlers: any; isRemoveFromTeamAllowed: boolean; + theme?: Theme; } const UsersTable: React.FC = ({ @@ -66,7 +70,8 @@ const UsersTable: React.FC = ({ org_id, useRemoveUserFromTeamMutation, useNotificationHandlers, - isRemoveFromTeamAllowed + isRemoveFromTeamAllowed, + theme }) => { const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(10); @@ -75,7 +80,6 @@ const UsersTable: React.FC = ({ const availableRoles: string[] = []; const { handleError, handleSuccess, handleInfo } = useNotificationHandlers(); const ref: any = useRef(null); - const { width } = useWindowDimensions(); const { data: userData } = useGetUsersForOrgQuery({ @@ -98,7 +102,8 @@ const UsersTable: React.FC = ({ const response = await ref.current?.show({ title: `Remove User From Team ?`, subtitle: removeUserFromTeamModalContent(data[3], data[2]), - primaryOption: 'Proceed' + primaryOption: 'Proceed', + variant: PROMPT_VARIANTS.DANGER }); if (response === 'Proceed') { removeUserFromTeam({ @@ -111,7 +116,6 @@ const UsersTable: React.FC = ({ handleSuccess(`${data[4] ? data[4] : ''} ${data[5] ? data[5] : ''} removed from team`); }) .catch((err: any) => { - console.log('heya err', err); const error = err.response?.data?.message || 'Failed to remove user from team'; if (err.response.status === 404) { handleInfo(error); @@ -127,20 +131,6 @@ const UsersTable: React.FC = ({ return rowData[columnIndex]; }; - // const fetchAvailableRoles = () => { - // axios - // .get(process.env.API_ENDPOINT_PREFIX + `/api/identity/orgs/${org_id}/roles?all=true`) - // .then((res) => { - // let roles = []; - // res?.data?.roles?.forEach((role) => roles.push(role?.role_name)); - // setAvailableRoles(roles); - // }) - // .catch((err) => { - // let error = err.response?.data?.message || 'Failed to fetch roles'; - // handleError(error); - // }); - // }; - const removeUserFromTeamModalContent = (user: string, email: string) => ( <>

Are you sure you want to remove this user? (This action is irreversible)

@@ -253,29 +243,12 @@ const UsersTable: React.FC = ({ searchable: false, customBodyRender: (value: string, tableMeta: MUIDataTableMeta) => ( img': { mr: 2, flexShrink: 0 } }}> - - - - { - window.open( - `/user/${getValidColumnValue(tableMeta.rowData, 'user_id', columns)}` - ); - }} - alt={getValidColumnValue(tableMeta.rowData, 'first_name', columns)} - src={value} - > - {value ? '' : } - - - - - {tableMeta.rowData[4]} {tableMeta.rowData[5]} - - {tableMeta.rowData[2]} - - - + ) } @@ -440,6 +413,7 @@ const UsersTable: React.FC = ({ tableMeta={tableMeta} handleRemoveFromTeam={handleRemoveFromTeam} isRemoveFromTeamAllowed={isRemoveFromTeamAllowed} + theme={theme} /> ) } @@ -457,9 +431,8 @@ const UsersTable: React.FC = ({ }); return initialVisibility; }); - return ( - +
= ({ tableCols={tableCols} updateCols={updateCols} columnVisibility={columnVisibility} + backgroundColor={theme?.palette.background.tabs} />
-
+ ); }; diff --git a/src/custom/UsersTable/index.ts b/src/custom/UsersTable/index.ts index ee80ef649..bab343d76 100644 --- a/src/custom/UsersTable/index.ts +++ b/src/custom/UsersTable/index.ts @@ -1,3 +1,3 @@ import UsersTable from './UsersTable'; - -export { UsersTable }; +import UserTableAvatarInfo from './UserTableAvatarInfo'; +export { UsersTable, UserTableAvatarInfo }; diff --git a/src/custom/Workspaces/DesignTable.tsx b/src/custom/Workspaces/DesignTable.tsx index 0ad01f4d9..c8c0a2f58 100644 --- a/src/custom/Workspaces/DesignTable.tsx +++ b/src/custom/Workspaces/DesignTable.tsx @@ -5,7 +5,7 @@ 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 { useTheme } from '../../theme'; import { CatalogDesignsTable, createDesignsColumnsConfig, @@ -18,10 +18,8 @@ import { updateVisibleColumns } from '../Helpers/ResponsiveColumns/responsive-co import PromptComponent from '../Prompt'; import SearchBar from '../SearchBar'; import AssignmentModal from './AssignmentModal'; -import EditButton from './EditButton'; import useDesignAssignment from './hooks/useDesignAssignment'; -import { TableHeader, TableRightActionHeader } from './styles'; - +import { L5EditIcon, TableHeader, TableRightActionHeader } from './styles'; export interface DesignTableProps { workspaceId: string; workspaceName: string; @@ -39,16 +37,17 @@ export interface DesignTableProps { workspaceName: string, workspaceId: string ) => void; - getDownloadUrl: (id: string) => string; handlePublish: (publishModal: PublishModalState, data: any) => void; publishModalHandler: any; handleUnpublishModal: (design: Pattern, modalRef: React.RefObject) => void; + handleDownload?: (design: Pattern) => void; handleBulkUnpublishModal: ( selected: any, designs: Pattern[], modalRef: React.RefObject ) => void; handleShowDetails: (designId: string, designName: string) => void; + getDownloadUrl?: (id: string) => string; GenericRJSFModal: any; isDownloadAllowed: boolean; isCopyLinkAllowed: boolean; @@ -81,10 +80,11 @@ const DesignTable: React.FC = ({ handleClone, handleCopyUrl, handlePublish, + handleDownload, + getDownloadUrl, handleShowDetails, handleUnpublishModal, handleWorkspaceDesignDeleteModal, - getDownloadUrl, publishModalHandler, isCopyLinkAllowed, isDeleteAllowed, @@ -116,7 +116,7 @@ const DesignTable: React.FC = ({ pattern: result }); }; - + const theme = useTheme(); const columns = createDesignsColumnsConfig({ handleDeleteModal: (design) => () => handleWorkspaceDesignDeleteModal(design.id, workspaceId), handlePublishModal, @@ -124,6 +124,7 @@ const DesignTable: React.FC = ({ handleCopyUrl, handleClone, handleShowDetails, + handleDownload, getDownloadUrl, isCopyLinkAllowed, isDeleteAllowed, @@ -131,7 +132,8 @@ const DesignTable: React.FC = ({ isPublishAllowed, isUnpublishAllowed, isFromWorkspaceTable: true, - isRemoveAllowed + isRemoveAllowed, + theme }); const [publishSchema, setPublishSchema] = useState<{ @@ -152,7 +154,7 @@ const DesignTable: React.FC = ({ return initialVisibility; }); - const [expanded, setExpanded] = useState(true); + const [expanded, setExpanded] = useState(false); const handleAccordionChange = () => { setExpanded(!expanded); }; @@ -184,7 +186,7 @@ const DesignTable: React.FC = ({ const tableHeaderContent = ( - + Assigned Designs @@ -207,13 +209,17 @@ const DesignTable: React.FC = ({ }} id={'catalog-table'} /> - + ); return ( - + <> } @@ -242,6 +248,7 @@ const DesignTable: React.FC = ({ } filter={'my-designs'} setSearch={setDesignSearch} + tableBackgroundColor={theme.palette.background.constant?.table} /> @@ -276,7 +283,7 @@ const DesignTable: React.FC = ({ buttonTitle="Publish" /> - + ); }; diff --git a/src/custom/Workspaces/EditButton.tsx b/src/custom/Workspaces/EditButton.tsx deleted file mode 100644 index 7486ae159..000000000 --- a/src/custom/Workspaces/EditButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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 index 79aa6739c..e79d6fa06 100644 --- a/src/custom/Workspaces/EnvironmentTable.tsx +++ b/src/custom/Workspaces/EnvironmentTable.tsx @@ -4,7 +4,7 @@ import { MUIDataTableColumn, 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 { useTheme } from '../../theme'; import { CustomColumnVisibilityControl } from '../CustomColumnVisibilityControl'; import { CustomTooltip } from '../CustomTooltip'; import { ConditionalTooltip } from '../Helpers/CondtionalTooltip'; @@ -17,9 +17,14 @@ import ResponsiveDataTable, { IconWrapper } from '../ResponsiveDataTable'; import SearchBar from '../SearchBar'; import { TooltipIcon } from '../TooltipIconButton'; import AssignmentModal from './AssignmentModal'; -import EditButton from './EditButton'; import useEnvironmentAssignment from './hooks/useEnvironmentAssignment'; -import { CellStyle, CustomBodyRenderStyle, TableHeader, TableRightActionHeader } from './styles'; +import { + CellStyle, + CustomBodyRenderStyle, + L5EditIcon, + TableHeader, + TableRightActionHeader +} from './styles'; interface EnvironmentTableProps { workspaceId: string; @@ -62,8 +67,9 @@ const EnvironmentTable: React.FC = ({ useAssignEnvironmentToWorkspaceMutation, isAssignAllowed }) => { - const [expanded, setExpanded] = useState(true); - const handleAccordionChange = () => { + const [expanded, setExpanded] = useState(false); + const handleAccordionChange = (e: React.SyntheticEvent) => { + e.stopPropagation(); setExpanded(!expanded); }; const [search, setSearch] = useState(''); @@ -79,6 +85,7 @@ const EnvironmentTable: React.FC = ({ order: sortOrder }); const { width } = useWindowDimensions(); + const theme = useTheme(); const [unassignEnvironmentFromWorkspace] = useUnassignEnvironmentFromWorkspaceMutation(); const columns: MUIDataTableColumn[] = [ { @@ -164,7 +171,7 @@ const EnvironmentTable: React.FC = ({ }} iconType="delete" > - + ) @@ -236,7 +243,7 @@ const EnvironmentTable: React.FC = ({ const [tableCols, updateCols] = useState(columns); return ( - + <> } @@ -245,7 +252,7 @@ const EnvironmentTable: React.FC = ({ }} > - + Assigned Environments @@ -268,9 +275,10 @@ const EnvironmentTable: React.FC = ({ }} id={'environments-table'} /> - @@ -308,7 +316,7 @@ const EnvironmentTable: React.FC = ({ isAssignAllowed={isAssignAllowed} isRemoveAllowed={isRemoveAllowed} /> - + ); }; diff --git a/src/custom/Workspaces/WorkspaceCard.tsx b/src/custom/Workspaces/WorkspaceCard.tsx new file mode 100644 index 000000000..d06340cfc --- /dev/null +++ b/src/custom/Workspaces/WorkspaceCard.tsx @@ -0,0 +1,372 @@ +import { useTheme } from '@mui/material'; +import { Backdrop, CircularProgress, Grid } from '../../base'; +import { FlipCard } from '../FlipCard'; +import { RecordRow, RedirectButton, TransferButton } from './WorkspaceTransferButton'; +import { formattoLongDate } from './helper'; +import { + AllocationColumnGrid, + AllocationWorkspace, + BulkSelectCheckbox, + CardBackActionsGrid, + CardBackTitleGrid, + CardBackTopGrid, + CardBackWrapper, + CardFrontWrapper, + CardTitle, + DateColumnGrid, + DateGrid, + DateLabel, + DescriptionLabel, + EmptyDescription, + L5DeleteIcon, + L5EditIcon, + RecentActivityGrid, + RecentActivityTitle, + WorkspaceCardGrid +} from './styles'; + +interface WorkspaceDetails { + id: number; + name: string; + description: string; + deleted_at: { Valid: boolean }; + updated_at: string; + created_at: string; +} + +type Activity = { + description: string; + first_name: string; + created_at: string; +}; + +interface CardFrontProps { + onFlip: () => void; + name: string; + description: string; + environmentsCount: number; + onAssignEnvironment: () => void; + teamsCount: number; + onAssignTeam: () => void; + designAndViewOfWorkspaceCount: number; + onAssignDesign: () => void; + isEnvironmentAllowed: boolean; + isTeamAllowed: boolean; + isDesignAndViewAllowed: boolean; +} + +interface CardBackProps { + onFlipBack: () => void; + onSelect: () => void; + name: string; + onEdit: () => void; + onDelete: () => void; + selectedWorkspaces: number[]; + workspaceId: number; + loadingEvents: boolean; + recentActivities: Activity[]; + updatedDate: string; + createdDate: string; + deleted: boolean; + isDeleteWorkspaceAllowed: boolean; + isEditWorkspaceAllowed: boolean; +} + +interface WorkspaceCardProps { + workspaceDetails: WorkspaceDetails; + onDelete: () => void; + onEdit: () => void; + onSelect: () => void; + selectedWorkspaces: number[]; + onAssignTeam: () => void; + onAssignEnvironment: () => void; + onAssignDesign: () => void; + recentActivities: Activity[]; + onFlip: () => void; + onFlipBack: () => void; + loadingEvents: boolean; + teamsOfWorkspaceCount: number; + environmentsOfWorkspaceCount: number; + designAndViewOfWorkspaceCount: number; + isEnvironmentAllowed: boolean; + isTeamAllowed: boolean; + isDesignAndViewAllowed: boolean; + isDeleteWorkspaceAllowed: boolean; + isEditWorkspaceAllowed: boolean; +} + +/** + * Renders a Workspace card component. + * + * @param {Object} props - The component props. + * @param {Object} props.environmentDetails - The details of the workspace. + * @param {string} props.environmentDetails.name - The name of the workspace. + * @param {string} props.environmentDetails.description - The description of the workspace. + * @param {Function} props.onDelete - Function to delete the workspace. + * @param {Function} props.onEdit - Function to edit the workspace. + * @param {Function} props.onSelect - Function to select workspace for bulk actions. + * @param {Array} props.selectedWorkspaces - Selected workspace list for delete. + * @param {Function} props.onAssignTeam - Function to open team assignment modal open. + * @param {Function} props.onAssignDesign - Function to open design assignment modal open. + * @param {Array} props.latestActivity - List of latest activity. + * @param {Function} props.onFlip - Click event to trigger when card flip. + * @param {Function} props.onFlipBack - Click event to trigger when card flip back. + * @param {Boolean} props.loadingEvents - Loading state of the events. + * @param {Number} props.teamsOfWorkspaceCount - Count of teams assigned to the workspace. + * @param {Number} props.environmentsOfWorkspaceCount - Count of environments assigned to the workspace. + * @param {Number} props.designAndViewOfWorkspaceCount - Count of designs/views assigned to the workspace. + * @param {Boolean} props.isEnvironmentAllowed - Flag to check if environment assignment is allowed. + * @param {Boolean} props.isTeamAllowed - Flag to check if team assignment is allowed. + * @param {Boolean} props.isDesignAndViewAllowed - Flag to check if design assignment is allowed. + * @param {Boolean} props.isDeleteWorkspaceAllowed - Flag to check if workspace deletion is allowed. + * @param {Boolean} props.isEditWorkspaceAllowed - Flag to check if workspace edit is allowed. + * @returns {React.ReactElement} The Workspace card component. + * + */ + +const WorkspaceCard = ({ + workspaceDetails, + onDelete, + onEdit, + onSelect, + selectedWorkspaces, + onAssignTeam, + onAssignEnvironment, + onAssignDesign, + recentActivities, + onFlip, + onFlipBack, + loadingEvents, + teamsOfWorkspaceCount, + environmentsOfWorkspaceCount, + designAndViewOfWorkspaceCount, + isEnvironmentAllowed, + isTeamAllowed, + isDesignAndViewAllowed, + isDeleteWorkspaceAllowed, + isEditWorkspaceAllowed +}: WorkspaceCardProps) => { + const deleted = workspaceDetails.deleted_at.Valid; + return ( + + + + + + ); +}; + +export default WorkspaceCard; + +const CardFront = ({ + onFlip, + name, + description, + environmentsCount, + onAssignEnvironment, + teamsCount, + onAssignTeam, + designAndViewOfWorkspaceCount, + onAssignDesign, + isEnvironmentAllowed, + isTeamAllowed, + isDesignAndViewAllowed +}: CardFrontProps) => { + return ( + + + e.stopPropagation()}> + {name} + + + + {description ? ( + e.stopPropagation()} sx={{ maxHeight: '105px' }}> + {description} + + ) : ( + e.stopPropagation()}>No description + )} + + + + e.stopPropagation()}> + {isEnvironmentAllowed ? ( + + ) : ( + + )} + + + + + + e.stopPropagation()}> + {isTeamAllowed ? ( + + ) : ( + + )} + + + + + e.stopPropagation()}> + {isDesignAndViewAllowed ? ( + + ) : ( + + )} + + + + + + ); +}; + +const CardBack = ({ + onFlipBack, + onSelect, + name, + onEdit, + onDelete, + selectedWorkspaces, + workspaceId, + loadingEvents, + recentActivities, + updatedDate, + createdDate, + deleted, + isDeleteWorkspaceAllowed, + isEditWorkspaceAllowed +}: CardBackProps) => { + const isWorkspaceSelected = selectedWorkspaces?.includes(workspaceId); + const isEditButtonDisabled = isWorkspaceSelected ? true : !isEditWorkspaceAllowed; + const isDeleteButtonDisabled = isWorkspaceSelected ? true : !isDeleteWorkspaceAllowed; + + const theme = useTheme(); + return ( + + + + e.stopPropagation()} + onChange={onSelect} + disabled={deleted ? true : !isDeleteWorkspaceAllowed} + /> + e.stopPropagation()} + > + {name} + + + + + + + + + Recent Activity + + + {loadingEvents ? ( + + + + ) : ( + recentActivities?.map((activity, index) => { + return ( + + ); + }) + )} + + + + e.stopPropagation()}> + Updated At: {formattoLongDate(updatedDate)} + + + + e.stopPropagation()}> + Created At: {formattoLongDate(createdDate)} + + + + + ); +}; diff --git a/src/custom/Workspaces/WorkspaceTeamsTable.tsx b/src/custom/Workspaces/WorkspaceTeamsTable.tsx index 61475377b..dd4a366d6 100644 --- a/src/custom/Workspaces/WorkspaceTeamsTable.tsx +++ b/src/custom/Workspaces/WorkspaceTeamsTable.tsx @@ -3,15 +3,14 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useState } from 'react'; import { Accordion, AccordionDetails, AccordionSummary, Typography } from '../../base'; import { TeamsIcon } from '../../icons'; -import { SistentThemeProvider } from '../../theme'; +import { useTheme } from '../../theme'; import { CustomColumnVisibilityControl } from '../CustomColumnVisibilityControl'; import SearchBar from '../SearchBar'; import { TeamTableConfiguration } from '../TeamTable'; import TeamTable from '../TeamTable/TeamTable'; import AssignmentModal from './AssignmentModal'; -import EditButton from './EditButton'; import useTeamAssignment from './hooks/useTeamAssignment'; -import { TableHeader, TableRightActionHeader } from './styles'; +import { L5EditIcon, TableHeader, TableRightActionHeader } from './styles'; export interface TeamsTableProps { workspaceId: string; @@ -51,7 +50,7 @@ const TeamsTable: React.FC = ({ const [pageSize, setPageSize] = useState(10); const [sortOrder, setSortOrder] = useState('updated_at desc'); const [bulkSelect, setBulkSelect] = useState(false); - const [expanded, setExpanded] = useState(true); + const [expanded, setExpanded] = useState(false); const handleAccordionChange = () => { setExpanded(!expanded); }; @@ -104,16 +103,16 @@ const TeamsTable: React.FC = ({ isDeleteTeamAllowed: isDeleteTeamAllowed, setSearch }); - + const theme = useTheme(); return ( - + <> } sx={{ backgroundColor: 'background.paper' }} > - + Assigned Teams @@ -136,9 +135,10 @@ const TeamsTable: React.FC = ({ }} id={'teams-table'} /> - @@ -165,7 +165,14 @@ const TeamsTable: React.FC = ({ open={teamAssignment.assignModal} onClose={teamAssignment.handleAssignModalClose} title={`Assign Teams to ${workspaceName}`} - headerIcon={} + headerIcon={ + + } name="Teams" assignableData={teamAssignment.data} handleAssignedData={teamAssignment.handleAssignData} @@ -175,7 +182,7 @@ const TeamsTable: React.FC = ({ height="5rem" width="5rem" primaryFill={'#808080'} - secondaryFill={'gray'} + secondaryFill={theme.palette.icon.disabled} fill={'#808080'} /> } @@ -189,7 +196,7 @@ const TeamsTable: React.FC = ({ isAssignAllowed={isAssignTeamAllowed} isRemoveAllowed={isRemoveTeamFromWorkspaceAllowed} /> - + ); }; diff --git a/src/custom/Workspaces/WorkspaceTransferButton.tsx b/src/custom/Workspaces/WorkspaceTransferButton.tsx new file mode 100644 index 000000000..7fb1cfeae --- /dev/null +++ b/src/custom/Workspaces/WorkspaceTransferButton.tsx @@ -0,0 +1,114 @@ +import { SyncAlt as SyncAltIcon } from '@mui/icons-material'; +import { Grid, Tooltip, Typography } from '../../base'; +import { useTheme } from '../../theme'; +import { formatShortDate, formatShortDateTime } from './helper'; +import { PopupButton, Record, TabCount, TabTitle } from './styles'; + +interface TransferButtonProps { + title: string; + count: number; + onAssign: () => void; + disabled: boolean; +} + +interface RedirectButtonProps { + title: string; + count: number; + disabled?: boolean; +} + +export const TransferButton: React.FC = ({ + title, + count, + onAssign, + disabled +}) => { + const theme = useTheme(); + return ( + + + {count} + {title} + + + + ); +}; + +export const RedirectButton: React.FC = ({ + title, + count, + disabled = true +}) => { + return ( + + + {count} + {title} + {/* */} + + + ); +}; + +interface RecordRowProps { + title: string; + name: string; + date?: string | Date; +} + +export const RecordRow: React.FC = ({ title, name, date }) => { + const theme = useTheme(); + + return ( + + + + {title} + + + {name} + + + + + + {date ? formatShortDate(date) : '-'} + + + + + ); +}; diff --git a/src/custom/Workspaces/WorkspaceViewsTable.tsx b/src/custom/Workspaces/WorkspaceViewsTable.tsx index 2335dcf00..af1cdc70d 100644 --- a/src/custom/Workspaces/WorkspaceViewsTable.tsx +++ b/src/custom/Workspaces/WorkspaceViewsTable.tsx @@ -2,9 +2,9 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { MUIDataTableColumn, MUIDataTableMeta } from 'mui-datatables'; import React, { useState } from 'react'; -import { Accordion, AccordionDetails, AccordionSummary, Typography } from '../../base'; +import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from '../../base'; import { DeleteIcon, EnvironmentIcon } from '../../icons'; -import { CHARCOAL, SistentThemeProvider } from '../../theme'; +import { useTheme } from '../../theme'; import { NameDiv } from '../CatalogDesignTable/style'; import { RESOURCE_TYPES } from '../CatalogDetail/types'; import { CustomColumnVisibilityControl } from '../CustomColumnVisibilityControl'; @@ -18,10 +18,16 @@ import { import ResponsiveDataTable, { IconWrapper } from '../ResponsiveDataTable'; import SearchBar from '../SearchBar'; import { TooltipIcon } from '../TooltipIconButton'; +import { UserTableAvatarInfo } from '../UsersTable'; import AssignmentModal from './AssignmentModal'; -import EditButton from './EditButton'; import useViewAssignment from './hooks/useViewsAssignment'; -import { CellStyle, CustomBodyRenderStyle, TableHeader, TableRightActionHeader } from './styles'; +import { + CellStyle, + CustomBodyRenderStyle, + L5EditIcon, + TableHeader, + TableRightActionHeader +} from './styles'; interface ViewsTableProps { workspaceId: string; @@ -36,10 +42,13 @@ interface ViewsTableProps { const colViews: ColView[] = [ ['id', 'na'], + ['avatar_url', 'xs'], + ['email', 'na'], ['name', 'xs'], - ['description', 'm'], - ['organization_id', 'l'], - ['created_at', 'xl'], + ['first_name', 'na'], + ['last_name', 'na'], + ['organization_id', 'xl'], + ['created_at', 'na'], ['updated_at', 'xl'], ['visibility', 'l'], ['actions', 'xs'] @@ -67,7 +76,8 @@ const WorkspaceViewsTable: React.FC = ({ isAssignAllowed, handleShowDetails }) => { - const [expanded, setExpanded] = useState(true); + const theme = useTheme(); + const [expanded, setExpanded] = useState(false); const handleAccordionChange = () => { setExpanded(!expanded); }; @@ -81,7 +91,8 @@ const WorkspaceViewsTable: React.FC = ({ page: page, pageSize: pageSize, search: search, - order: sortOrder + order: sortOrder, + expandUser: true }); const { width } = useWindowDimensions(); const [unassignviewFromWorkspace] = useUnassignViewFromWorkspaceMutation(); @@ -112,6 +123,62 @@ const WorkspaceViewsTable: React.FC = ({ } } }, + { + name: 'avatar_url', + label: 'Owner', + options: { + filter: false, + sort: false, + searchable: false, + customBodyRender: (value: string, tableMeta: MUIDataTableMeta) => { + const getValidColumnValue = ( + rowData: any, + columnName: string, + columns: MUIDataTableColumn[] + ) => { + const columnIndex = columns.findIndex((column: any) => column.name === columnName); + return rowData[columnIndex]; + }; + return ( + img': { mr: 2, flexShrink: 0 } }}> + + + ); + } + } + }, + { + name: 'email', + label: 'Email', + options: { + filter: false, + sort: true, + searchable: true + } + }, + { + name: 'first_name', + label: 'First Name', + options: { + filter: false, + sort: true, + searchable: true + } + }, + { + name: 'last_name', + label: 'Last Name', + options: { + filter: false, + sort: true, + searchable: true + } + }, { name: 'created_at', label: 'Created At', @@ -169,7 +236,7 @@ const WorkspaceViewsTable: React.FC = ({ }} iconType="delete" > - + ) @@ -236,7 +303,7 @@ const WorkspaceViewsTable: React.FC = ({ const [tableCols, updateCols] = useState(columns); return ( - + <> } @@ -245,7 +312,7 @@ const WorkspaceViewsTable: React.FC = ({ }} > - + Assigned Views @@ -268,7 +335,11 @@ const WorkspaceViewsTable: React.FC = ({ }} id={'views-table'} /> - + @@ -305,7 +376,7 @@ const WorkspaceViewsTable: React.FC = ({ isAssignAllowed={isAssignAllowed} isRemoveAllowed={isRemoveAllowed} /> - + ); }; diff --git a/src/custom/Workspaces/helper.ts b/src/custom/Workspaces/helper.ts index 553442794..23e613b2f 100644 --- a/src/custom/Workspaces/helper.ts +++ b/src/custom/Workspaces/helper.ts @@ -17,3 +17,65 @@ export const parseDeletionTimestamp = (data: { return DEFAULT_DATE; } }; + +/** + * Formats a date into a short date-time string (e.g., "Jan 1, 2024, 09:30 AM") + * + * @param {Date | string} date - The date to format. Can be a Date object or date string + * @returns {string} Formatted date string in the format "MMM D, YYYY, HH:MM AM/PM" + * + * @example + * formatShortDateTime("2024-01-01T09:30:00") // Returns "Jan 1, 2024, 09:30 AM" + * formatShortDateTime(new Date()) // Returns current date-time in short format + * + * Generated by Copilot + */ +export const formatShortDateTime = (date: Date | string): string => { + return new Date(date).toLocaleDateString('en-US', { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +}; + +/** + * Formats a date into a short date string (e.g., "Jan 1, 2024") + * + * @param {Date | string} date - The date to format. Can be a Date object or date string + * @returns {string} Formatted date string in the format "MMM D, YYYY" + * + * @example + * formatShortDate("2024-01-01") // Returns "Jan 1, 2024" + * formatShortDate(new Date()) // Returns current date in short format + * + * Generated by Copilot + */ +export const formatShortDate = (date: Date | string): string => { + return new Date(date).toLocaleDateString('en-US', { + day: 'numeric', + month: 'short', + year: 'numeric' + }); +}; + +/** + * Formats a date into a long date string (e.g., "January 1, 2024") + * + * @param {Date | string} date - The date to format. Can be a Date object or date string + * @returns {string} Formatted date string in the format "MMMM D, YYYY" + * + * @example + * formattoLongDate("2024-01-01") // Returns "January 1, 2024" + * formattoLongDate(new Date()) // Returns current date in long format + * + * Generated by Copilot + */ +export const formattoLongDate = (date: Date | string): string => { + return new Date(date).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric' + }); +}; diff --git a/src/custom/Workspaces/hooks/useDesignAssignment.tsx b/src/custom/Workspaces/hooks/useDesignAssignment.tsx index 31f268ff9..4f6f5fb33 100644 --- a/src/custom/Workspaces/hooks/useDesignAssignment.tsx +++ b/src/custom/Workspaces/hooks/useDesignAssignment.tsx @@ -95,9 +95,6 @@ const useDesignAssignment = ({ }; const getAddedAndRemovedDesigns = (allAssignedDesigns: Pattern[]): AddedAndRemovedDesigns => { - if (Array.isArray(workspaceDesignsData) && workspaceDesignsData.length === 0) { - return { addedDesignsIds: [], removedDesignsIds: [] }; - } const originalDesignsIds = workspaceDesignsData.map((design) => design.id); const updatedDesignsIds = allAssignedDesigns.map((design) => design.id); diff --git a/src/custom/Workspaces/index.ts b/src/custom/Workspaces/index.ts index 82cd1eacd..7215852a7 100644 --- a/src/custom/Workspaces/index.ts +++ b/src/custom/Workspaces/index.ts @@ -1,17 +1,22 @@ import AssignmentModal from './AssignmentModal'; import DesignTable from './DesignTable'; import EnvironmentTable from './EnvironmentTable'; +import WorkspaceCard from './WorkspaceCard'; import WorkspaceTeamsTable from './WorkspaceTeamsTable'; import WorkspaceViewsTable from './WorkspaceViewsTable'; import useDesignAssignment from './hooks/useDesignAssignment'; import useEnvironmentAssignment from './hooks/useEnvironmentAssignment'; import useTeamAssignment from './hooks/useTeamAssignment'; import useViewAssignment from './hooks/useViewsAssignment'; +import { L5DeleteIcon, L5EditIcon } from './styles'; export { AssignmentModal, DesignTable, EnvironmentTable, + L5DeleteIcon, + L5EditIcon, + WorkspaceCard, WorkspaceTeamsTable, WorkspaceViewsTable, useDesignAssignment, diff --git a/src/custom/Workspaces/styles.ts b/src/custom/Workspaces/styles.ts deleted file mode 100644 index c26b225ab..000000000 --- a/src/custom/Workspaces/styles.ts +++ /dev/null @@ -1,112 +0,0 @@ -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%' -}); - -export const TableTopIcon = styled('span')(() => ({ - '& svg': { - cursor: 'pointer', - width: '2rem', - height: '2rem' - } -})); - -export const DisabledTableTopIcon = styled('span')(() => ({ - '& svg': { - width: '2rem', - height: '2rem' - } -})); - -export const MesheryDeleteIcon = styled('span')(({ theme }) => ({ - '& svg': { - color: '#3C494F', - '&:hover': { - color: theme.palette.error.error - } - } -})); - -export const TableIconsDisabledContainer = styled('span')(() => ({ - color: '#455a64', - opacity: '0.5', - '& svg': { - cursor: 'not-allowed' - } -})); - -export const TableTopIconsWrapper = styled('div')(() => ({ - display: 'flex', - justifyContent: 'space-between', - paddingRight: '26px' -})); - -export const TableIconsContainer = styled('div')(() => ({ - color: '#455a64', - display: 'flex', - cursor: 'not-allowed', - '& svg': { - cursor: 'pointer' - } -})); diff --git a/src/custom/Workspaces/styles.tsx b/src/custom/Workspaces/styles.tsx new file mode 100644 index 000000000..0fdea72a3 --- /dev/null +++ b/src/custom/Workspaces/styles.tsx @@ -0,0 +1,415 @@ +import { Box, Button, Card, Checkbox, Grid, IconButton, Typography } from '../../base'; +import { DeleteIcon, EditIcon } from '../../icons'; +import { styled, useTheme } from '../../theme'; +import { charcoal } from '../../theme/colors/colors'; +import { CustomTooltip } from '../CustomTooltip'; + +export const ModalActionDiv = styled('div')({ + display: 'flex', + gap: '1rem' +}); + +interface ExtendedEditIconProps { + onClick: () => void; + disabled?: boolean; + bulk?: boolean; + style?: React.CSSProperties; + title?: string; +} + +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%' +}); + +export const TableTopIcon = styled('span')(() => ({ + '& svg': { + cursor: 'pointer', + width: '2rem', + height: '2rem' + } +})); + +export const DisabledTableTopIcon = styled('span')(() => ({ + '& svg': { + width: '2rem', + height: '2rem' + } +})); + +export const MesheryDeleteIcon = styled('span')(({ theme }) => ({ + '& svg': { + color: '#3C494F', + '&:hover': { + color: theme.palette.error.error + } + } +})); + +export const TableIconsDisabledContainer = styled('span')(() => ({ + color: '#455a64', + opacity: '0.5', + '& svg': { + cursor: 'not-allowed' + } +})); + +export const TableTopIconsWrapper = styled('div')(() => ({ + display: 'flex', + justifyContent: 'space-between', + paddingRight: '26px' +})); + +export const TableIconsContainer = styled('div')(() => ({ + color: '#455a64', + display: 'flex', + cursor: 'not-allowed', + '& svg': { + cursor: 'pointer' + } +})); + +export const BulkSelectCheckbox = styled(Checkbox)({ + padding: 0, + marginRight: '0.5rem', + height: '28px', + '& .MuiSvgIcon-root': { + borderColor: 'white' + }, + color: 'white', + '&:hover': { + color: 'white', + cursor: 'pointer' + }, + '&.Mui-checked': { + color: 'white' + } +}); + +export const CardTitle = styled(Typography)({ + fontSize: '1.25rem', + fontWeight: 800, + '&:hover': { + cursor: 'default' + } +}); + +export const OrganizationName = styled(Typography)({ + fontSize: '0.9rem', + display: 'flex', + alignItems: 'end', + padding: '0 5px', + '&:hover': { + cursor: 'default' + } +}); + +export const StyledIconButton = styled('button')({ + background: 'transparent', + border: 'none', + '&:hover': { + cursor: 'default' + } +}); + +export const DateLabel = styled(Typography)({ + fontStyle: 'italic', + fontSize: '12px', + '&:hover': { + cursor: 'default' + } +}); + +export const EmptyDescription = styled(Typography)({ + fontSize: '0.9rem', + textAlign: 'left', + fontStyle: 'italic' +}); + +export const DescriptionLabel = styled(EmptyDescription)({ + height: 'fit-content', + fontStyle: 'normal', + '&:hover': { + cursor: 'default' + } +}); + +export const AllocationButton = styled(Box)(({ theme }) => ({ + background: theme.palette.background.brand?.default, + padding: '10px 10px 1px 10px', + borderRadius: '4px', + height: '100%', + display: 'flex', + width: '100%' +})); + +export const AllocationWorkspace = styled(AllocationButton)({ + display: 'flex', + width: '100%', + gap: '10px', + ['@media (min-width : 600px)']: { + flexDirection: 'column', + gap: '0' + } +}); + +export const PopupButton = styled(Button)(({ theme }) => ({ + width: '100%', + borderRadius: '4px', + background: theme.palette.background.brand?.default, + boxShadow: '0px 4px 4px 0px rgba(0, 0, 0, 0.25)', + display: 'flex', + flexDirection: 'column', + marginBottom: '10px', + color: theme.palette.text.default, + '&:hover': { + background: theme.palette.text.default + }, + padding: '15px 10px' +})); + +interface TabStyleProps { + textColor?: string; +} + +export const TabTitle = styled('p')(({ theme, textColor }) => ({ + margin: '0', + fontSize: '14px', + fontWeight: '400', + display: 'flex', + color: textColor || theme.palette.text.constant?.white +})); + +export const TabCount = styled('p')(({ theme, textColor }) => ({ + margin: '0', + fontSize: '60px', + fontWeight: '500', + lineHeight: 1, + marginBottom: '5px', + color: textColor || theme.palette.text.constant?.white +})); + +export const ViewButton = styled(Button)(({ theme }) => ({ + width: '100%', + borderRadius: '4px', + background: theme.palette.text.default, + boxShadow: '0px 4px 4px 0px rgba(0, 0, 0, 0.25)', + display: 'flex', + flexDirection: 'column', + marginBottom: '10px', + color: `${charcoal[40]}30 !important`, + '&:hover': { + background: theme.palette.text.default + }, + padding: '15px 10px' +})); + +interface IconWrapperProps { + disabled?: boolean; +} + +export const IconWrapper = styled('div')(({ disabled = false }) => ({ + cursor: disabled ? 'not-allowed' : 'pointer', + opacity: disabled ? '0.5' : '1', + display: 'flex', + '& svg': { + cursor: disabled ? 'not-allowed' : 'pointer' + } +})); + +export const Record = styled(Grid)(({ theme }) => ({ + borderBottom: `1px solid ${theme.palette.divider}`, + display: 'flex', + flexDirection: 'row', + padding: '5px 0' +})); + +export const L5DeleteIcon = ({ + onClick, + bulk, + disabled, + style, + key, + title = 'Delete' +}: { + onClick: () => void; + bulk?: boolean; + disabled?: boolean; + style?: React.CSSProperties; + key?: string; + title?: string; +}) => { + const theme = useTheme(); + return ( + +
+ + + +
+
+ ); +}; + +export const L5EditIcon = ({ + onClick, + disabled, + bulk, + style, + title = 'Edit' +}: ExtendedEditIconProps) => { + const theme = useTheme(); + return ( + +
+ + + +
+
+ ); +}; + +export const WorkspaceCardGrid = styled(Grid)({ + display: 'flex', + flexDirection: 'row' +}); + +export const DescriptionGrid = styled(Grid)({ + display: 'flex', + alignItems: 'center', + marginTop: 1 +}); + +export const AllocationColumnGrid = styled(Grid)({ + width: '-moz-available' +}); + +export const CardWrapper = styled(Card)(({ theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(2.5), + cursor: 'pointer' +})); + +export const CardBackWrapper = styled(CardWrapper)(({ theme }) => ({ + minHeight: theme.spacing(50), + background: 'linear-gradient(180deg, #007366 0%, #000 100%)' +})); + +export const CardFrontWrapper = styled(CardWrapper)(({ theme }) => ({ + minHeight: theme.spacing(50), + + backgroundColor: theme.palette.background.paper, + boxShadow: 'none' +})); + +export const CardBackTopGrid = styled(Grid)({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between' +}); + +export const CardBackTitleGrid = styled(Grid)({ + display: 'flex', + alignItems: 'flex-start' +}); + +export const CardBackActionsGrid = styled(Grid)({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end' +}); + +export const RecentActivityTitle = styled(Typography)(({ theme }) => ({ + fontSize: '1.25rem', + fontWeight: 600, + padding: '0.5rem 0', + color: theme.palette.background.constant?.white +})); + +export const RecentActivityGrid = styled(Grid)({ + display: 'flex', + flexDirection: 'column', + maxHeight: '14.5rem', + overflowY: 'scroll' +}); + +export const DateGrid = styled(Grid)(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + position: 'absolute', + bottom: '20px', + width: '100%', + color: `${theme.palette.background.constant?.white}99`, + justifyContent: 'space-between', + paddingRight: '40px' +})); + +export const DateColumnGrid = styled(Grid)({ + textAlign: 'left' +}); diff --git a/src/custom/index.tsx b/src/custom/index.tsx index cc47f9dd8..4aeb3fdb2 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -47,7 +47,7 @@ import { TooltipIcon } from './TooltipIconButton'; import { TransferList } from './TransferModal/TransferList'; import { TransferListProps } from './TransferModal/TransferList/TransferList'; import UniversalFilter, { UniversalFilterProps } from './UniversalFilter'; -import { UsersTable } from './UsersTable'; +import { UserTableAvatarInfo, UsersTable } from './UsersTable'; import { VisibilityChipMenu } from './VisibilityChipMenu'; export { CatalogCard } from './CatalogCard'; export { CatalogFilterSidebar } from './CatalogFilterSection'; @@ -104,6 +104,7 @@ export { TooltipIcon, TransferList, UniversalFilter, + UserTableAvatarInfo, UsersTable, VisibilityChipMenu, updateVisibleColumns, diff --git a/src/icons/Delete/DeleteIcon.tsx b/src/icons/Delete/DeleteIcon.tsx index 31d3a5500..0877592e3 100644 --- a/src/icons/Delete/DeleteIcon.tsx +++ b/src/icons/Delete/DeleteIcon.tsx @@ -8,14 +8,15 @@ export const DeleteIcon = ({ style, ...props }: IconProps): JSX.Element => { + const _finalFill = style?.fill || fill; + return ( diff --git a/src/icons/View/ViewIcon.tsx b/src/icons/View/ViewIcon.tsx new file mode 100644 index 000000000..0ccc69c28 --- /dev/null +++ b/src/icons/View/ViewIcon.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react'; +import { IconProps } from '../types'; + +const ViewsIcon: FC = ({ width, height, style = {}, fill }) => ( + + + + + + + + + + + + +); + +export default ViewsIcon; diff --git a/src/icons/View/index.ts b/src/icons/View/index.ts new file mode 100644 index 000000000..07e933a55 --- /dev/null +++ b/src/icons/View/index.ts @@ -0,0 +1 @@ +export { default as ViewIcon } from './ViewIcon'; diff --git a/src/icons/index.ts b/src/icons/index.ts index 518f9d688..e162d2c1f 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -104,6 +104,7 @@ export * from './Tropy'; export * from './Undeploy'; export * from './Undo'; export * from './Validate'; +export * from './View'; export * from './Visibility'; export * from './Visualizer'; export * from './Workspace'; diff --git a/src/schemas/importDesign/schema.tsx b/src/schemas/importDesign/schema.tsx index d8dd15342..d33f2c113 100644 --- a/src/schemas/importDesign/schema.tsx +++ b/src/schemas/importDesign/schema.tsx @@ -5,157 +5,73 @@ const importDesignSchema = { type: 'string', title: 'Design file name', default: 'Untitled Design', - 'x-rjsf-grid-area': '6', + 'x-rjsf-grid-area': '12', description: 'Provide a name for your design file. This name will help you identify the file more easily. You can also change the name of your design after importing it.' }, - designType: { - title: 'Design type', - enum: ['Helm Chart', 'Kubernetes Manifest', 'Docker Compose', 'Meshery Design'], - 'x-rjsf-grid-area': '6', + // designType: { + // title: 'Design type', + // enum: ['Helm Chart', 'Kubernetes Manifest', 'Docker Compose', 'Meshery Design'], + // 'x-rjsf-grid-area': '6', + // description: + // "Select the type of design you are uploading. The 'Design Type' determines the format, structure, and content of the file you are uploading. Choose the appropriate design type that matches the nature of your file. Checkout https://docs.meshery.io/guides/configuration-management/creating-a-meshery-design to learn more about designs" + // }, + + uploadType: { + title: 'Upload method', + enum: ['File Upload', 'URL Import'], + default: 'URL Import', + 'x-rjsf-grid-area': '12', description: - "Select the type of design you are uploading. The 'Design Type' determines the format, structure, and content of the file you are uploading. Choose the appropriate design type that matches the nature of your file. Checkout https://docs.meshery.io/guides/configuration-management/creating-a-meshery-design to learn more about designs" + "Choose the method you prefer to upload your design file. Select 'File Upload' if you have the file on your local system, or 'URL Import' if you have the file hosted online." } }, + allOf: [ { if: { properties: { - designType: { - const: 'Helm Chart' + uploadType: { + const: 'File Upload' } } }, then: { properties: { - uploadType: { - title: 'Upload method', - enum: ['File Upload', 'URL Import'], - default: 'URL Import', - 'x-rjsf-grid-area': '12', - description: - "Choose the method you prefer to upload your Helm Chart design file. Select 'File Upload' if you have the file on your local system, or 'URL Import' if you have the file hosted online." + file: { + type: 'string', + format: 'file', + description: 'Browse the file from your file system', + 'x-rjsf-grid-area': '12' } }, - allOf: [ - { - if: { - properties: { - uploadType: { - const: 'File Upload' - } - } - }, - then: { - properties: { - file: { - type: 'string', - format: 'file', - description: 'Browse the Helm Chart file from your file system', - 'x-rjsf-grid-area': '12' - } - }, - required: ['file'] - } - }, - { - if: { - properties: { - uploadType: { - const: 'URL Import' - } - } - }, - then: { - properties: { - url: { - type: 'string', - format: 'uri', - title: 'URL', - description: - 'Provide the URL of the Helm Chart design file you want to import. This should be a direct URL to the file, for example: https://raw.github.com/your-design-file.yaml', - 'x-rjsf-grid-area': '12' - } - }, - required: ['url'] - } - } - ], - required: ['uploadType'] + required: ['file'] } }, { if: { properties: { - designType: { - not: { - const: 'Helm Chart' - } + uploadType: { + const: 'URL Import' } } }, then: { properties: { - uploadType: { - title: 'Upload method', - enum: ['File Upload', 'URL Import'], - default: 'URL Import', - 'x-rjsf-grid-area': '12', + url: { + type: 'string', + format: 'uri', + title: 'URL', description: - "Choose the method you prefer to upload your design file. Select 'File Upload' if you have the file on your local system, or 'URL Import' if you have the file hosted online." + 'Provide the URL of the file you want to import. This should be a direct URL to the file, for example: https://raw.github.com/your-design-file.yaml', + 'x-rjsf-grid-area': '12' } }, - allOf: [ - { - if: { - properties: { - uploadType: { - const: 'File Upload' - } - } - }, - then: { - properties: { - file: { - type: 'string', - format: 'file', - description: 'Browse the design file from your file system', - 'x-rjsf-grid-area': '12' - } - }, - required: ['file'] - } - }, - { - if: { - properties: { - uploadType: { - const: 'URL Import' - } - } - }, - then: { - properties: { - url: { - type: 'string', - format: 'uri', - title: 'URL', - description: - 'Provide the URL of the design file you want to import. This should be a direct URL to the file, for example: https://raw.github.com/your-design-file.yaml', - 'x-rjsf-grid-area': '12' - } - }, - required: ['url'] - } - } - ], - required: ['uploadType'] + required: ['url'] } - }, - { - required: ['designType'] } - ] + ], + required: ['uploadType', 'name'] }; export default importDesignSchema; diff --git a/src/schemas/importDesign/uiSchema.tsx b/src/schemas/importDesign/uiSchema.tsx index a2d409a1b..538272477 100644 --- a/src/schemas/importDesign/uiSchema.tsx +++ b/src/schemas/importDesign/uiSchema.tsx @@ -2,7 +2,7 @@ const importDesignUiSchema = { uploadType: { 'ui:widget': 'radio' }, - 'ui:order': ['name', 'designType', 'uploadType', 'file', 'url'] + 'ui:order': ['name', 'uploadType', 'file', 'url'] }; export default importDesignUiSchema;