mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-16 11:23:32 +00:00
feat(customHomePage): add skeleton for adding new modules to a template (#13994)
This commit is contained in:
parent
3c717bac36
commit
296b2d56cd
@ -44,6 +44,13 @@ const meta = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
menu: {
|
||||||
|
description: 'The menu props from Antd',
|
||||||
|
},
|
||||||
|
resetDefaultMenuStyles: {
|
||||||
|
description:
|
||||||
|
'Adds styles to reset the default styles in the menu. It works only with the main dropdown. To reset styles in the child popup set popupClassName to RESET_DROPDOWN_MENU_STYLES_CLASSNAME',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Define defaults
|
// Define defaults
|
||||||
|
|||||||
@ -1,18 +1,34 @@
|
|||||||
|
// Reset antd styles for the menu dropdown including submenu
|
||||||
|
// Styles can't be modified by styled as submenu is rendering in another portal
|
||||||
|
import '@components/components/Dropdown/reset-dropdown-menu-styles.less';
|
||||||
|
|
||||||
import { Dropdown as AntdDropdown } from 'antd';
|
import { Dropdown as AntdDropdown } from 'antd';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { RESET_DROPDOWN_MENU_STYLES_CLASSNAME } from '@components/components/Dropdown/constants';
|
||||||
import { DropdownProps } from '@components/components/Dropdown/types';
|
import { DropdownProps } from '@components/components/Dropdown/types';
|
||||||
import { useOverlayClassStackContext } from '@components/components/Utils/OverlayClassContext/OverlayClassContext';
|
import { useOverlayClassStackContext } from '@components/components/Utils/OverlayClassContext/OverlayClassContext';
|
||||||
|
|
||||||
export default function Dropdown({ children, overlayClassName, ...props }: React.PropsWithChildren<DropdownProps>) {
|
export default function Dropdown({
|
||||||
|
children,
|
||||||
|
overlayClassName,
|
||||||
|
resetDefaultMenuStyles,
|
||||||
|
...props
|
||||||
|
}: React.PropsWithChildren<DropdownProps>) {
|
||||||
// Get all overlay classes from parents
|
// Get all overlay classes from parents
|
||||||
const overlayClassNames = useOverlayClassStackContext();
|
const overlayClassNames = useOverlayClassStackContext();
|
||||||
|
|
||||||
const finalOverlayClassName = useMemo(() => {
|
const finalOverlayClassName = useMemo(() => {
|
||||||
|
const overlayClassNamesWithDefault = [
|
||||||
|
...overlayClassNames,
|
||||||
|
...(resetDefaultMenuStyles ? [RESET_DROPDOWN_MENU_STYLES_CLASSNAME] : []),
|
||||||
|
];
|
||||||
|
|
||||||
if (overlayClassName) {
|
if (overlayClassName) {
|
||||||
return [...overlayClassNames, overlayClassName].join(' ');
|
return [...overlayClassNamesWithDefault, overlayClassName].join(' ');
|
||||||
}
|
}
|
||||||
return overlayClassNames.join(' ');
|
return overlayClassNamesWithDefault.join(' ');
|
||||||
}, [overlayClassName, overlayClassNames]);
|
}, [overlayClassName, overlayClassNames, resetDefaultMenuStyles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AntdDropdown trigger={['click']} {...props} overlayClassName={finalOverlayClassName}>
|
<AntdDropdown trigger={['click']} {...props} overlayClassName={finalOverlayClassName}>
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const RESET_DROPDOWN_MENU_STYLES_CLASSNAME = 'datahub-reset-dropdown-menu-styles';
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
.datahub-reset-dropdown-menu-styles {
|
||||||
|
& .ant-dropdown-menu {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 16px; // compensation of padding as it computes incorrect position of a submenu
|
||||||
|
}
|
||||||
|
|
||||||
|
& .ant-dropdown-menu-submenu,
|
||||||
|
& .ant-dropdown-menu-item,
|
||||||
|
& .ant-dropdown-menu-item-group-list,
|
||||||
|
& .ant-dropdown-menu-item-group-title,
|
||||||
|
& .ant-dropdown-menu-submenu-title {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,5 +2,15 @@ import { DropdownProps as AntdDropdwonProps } from 'antd';
|
|||||||
|
|
||||||
export type DropdownProps = Pick<
|
export type DropdownProps = Pick<
|
||||||
AntdDropdwonProps,
|
AntdDropdwonProps,
|
||||||
'open' | 'overlayClassName' | 'disabled' | 'dropdownRender' | 'onOpenChange' | 'placement'
|
| 'open'
|
||||||
>;
|
| 'overlayClassName'
|
||||||
|
| 'disabled'
|
||||||
|
| 'dropdownRender'
|
||||||
|
| 'onOpenChange'
|
||||||
|
| 'placement'
|
||||||
|
| 'menu'
|
||||||
|
| 'trigger'
|
||||||
|
| 'destroyPopupOnHide'
|
||||||
|
> & {
|
||||||
|
resetDefaultMenuStyles?: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useGlobalSettings } from '@app/context/GlobalSettingsContext';
|
|||||||
import { useUserContext } from '@app/context/useUserContext';
|
import { useUserContext } from '@app/context/useUserContext';
|
||||||
import { Announcements } from '@app/homeV3/announcements/Announcements';
|
import { Announcements } from '@app/homeV3/announcements/Announcements';
|
||||||
import { CenteredContainer, ContentContainer, ContentDiv } from '@app/homeV3/styledComponents';
|
import { CenteredContainer, ContentContainer, ContentDiv } from '@app/homeV3/styledComponents';
|
||||||
import TemplateRow from '@app/homeV3/templateRow/TemplateRow';
|
import Template from '@app/homeV3/template/Template';
|
||||||
|
|
||||||
const HomePageContent = () => {
|
const HomePageContent = () => {
|
||||||
const { settings } = useGlobalSettings();
|
const { settings } = useGlobalSettings();
|
||||||
@ -17,10 +17,7 @@ const HomePageContent = () => {
|
|||||||
<CenteredContainer>
|
<CenteredContainer>
|
||||||
<ContentDiv>
|
<ContentDiv>
|
||||||
<Announcements />
|
<Announcements />
|
||||||
{template?.properties.rows.map((row, i) => {
|
<Template template={template} />
|
||||||
const key = `templateRow-${i}`;
|
|
||||||
return <TemplateRow key={key} row={row} />;
|
|
||||||
})}
|
|
||||||
</ContentDiv>
|
</ContentDiv>
|
||||||
</CenteredContainer>
|
</CenteredContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
|
|||||||
@ -63,6 +63,8 @@ vi.mock('@components', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const MOCKED_TIMESTAMP = 1752056099724;
|
||||||
|
|
||||||
describe('LargeModule', () => {
|
describe('LargeModule', () => {
|
||||||
const mockModule: ModuleProps['module'] = {
|
const mockModule: ModuleProps['module'] = {
|
||||||
urn: 'urn:li:dataHubPageModule:test',
|
urn: 'urn:li:dataHubPageModule:test',
|
||||||
@ -73,6 +75,13 @@ describe('LargeModule', () => {
|
|||||||
visibility: {
|
visibility: {
|
||||||
scope: PageModuleScope.Global,
|
scope: PageModuleScope.Global,
|
||||||
},
|
},
|
||||||
|
created: {
|
||||||
|
time: MOCKED_TIMESTAMP,
|
||||||
|
},
|
||||||
|
lastModified: {
|
||||||
|
time: MOCKED_TIMESTAMP,
|
||||||
|
},
|
||||||
|
params: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { DEFAULT_MODULE_ICON, MODULE_TYPE_TO_DESCRIPTION, MODULE_TYPE_TO_ICON } from '@app/homeV3/modules/constants';
|
||||||
|
import {
|
||||||
|
convertModuleToModuleInfo,
|
||||||
|
getModuleDescription,
|
||||||
|
getModuleIcon,
|
||||||
|
getModuleName,
|
||||||
|
getModuleType,
|
||||||
|
} from '@app/homeV3/modules/utils';
|
||||||
|
|
||||||
|
import { DataHubPageModule, DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
||||||
|
|
||||||
|
const MOCKED_TIMESTAMP = 1752056099724;
|
||||||
|
|
||||||
|
const MOCKED_MODULE: DataHubPageModule = {
|
||||||
|
urn: 'urn:li:dataHubPageModule:example',
|
||||||
|
type: EntityType.DatahubPageModule,
|
||||||
|
properties: {
|
||||||
|
created: {
|
||||||
|
time: MOCKED_TIMESTAMP,
|
||||||
|
},
|
||||||
|
lastModified: {
|
||||||
|
time: MOCKED_TIMESTAMP,
|
||||||
|
},
|
||||||
|
name: 'Link 1 (example)',
|
||||||
|
type: DataHubPageModuleType.Link,
|
||||||
|
visibility: {
|
||||||
|
scope: PageModuleScope.Global,
|
||||||
|
},
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('getModuleType', () => {
|
||||||
|
it('should return the correct type from module.properties.type', () => {
|
||||||
|
const module = MOCKED_MODULE;
|
||||||
|
expect(getModuleType(module)).toBe(module.properties.type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getModuleIcon', () => {
|
||||||
|
it('should return the corresponding icon from MODULE_TYPE_TO_ICON', () => {
|
||||||
|
const module = {
|
||||||
|
...MOCKED_MODULE,
|
||||||
|
...{ properties: { ...MOCKED_MODULE.properties, type: DataHubPageModuleType.Domains } },
|
||||||
|
};
|
||||||
|
const expectedIcon = MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.Domains);
|
||||||
|
expect(getModuleIcon(module)).toBe(expectedIcon);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the default icon when the type is not found', () => {
|
||||||
|
const module = {
|
||||||
|
...MOCKED_MODULE,
|
||||||
|
...{ properties: { ...MOCKED_MODULE.properties, type: 'UnknownType' as DataHubPageModuleType } },
|
||||||
|
};
|
||||||
|
expect(getModuleIcon(module)).toBe(DEFAULT_MODULE_ICON);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getModuleName', () => {
|
||||||
|
it('should return the module name from properties', () => {
|
||||||
|
const module = MOCKED_MODULE;
|
||||||
|
expect(getModuleName(module)).toBe(module.properties.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getModuleDescription', () => {
|
||||||
|
it('should return description from MODULE_TYPE_TO_DESCRIPTION map', () => {
|
||||||
|
const module = MOCKED_MODULE;
|
||||||
|
const moduleType = getModuleType(module);
|
||||||
|
const expectedDescription = MODULE_TYPE_TO_DESCRIPTION.get(moduleType);
|
||||||
|
expect(getModuleDescription(module)).toBe(expectedDescription);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertModuleToModuleInfo', () => {
|
||||||
|
it('should correctly convert DataHubPageModule to ModuleInfo', () => {
|
||||||
|
const module = MOCKED_MODULE;
|
||||||
|
const moduleInfo = convertModuleToModuleInfo(module);
|
||||||
|
|
||||||
|
expect(moduleInfo).toEqual({
|
||||||
|
urn: module.urn,
|
||||||
|
key: module.urn,
|
||||||
|
type: getModuleType(module),
|
||||||
|
name: getModuleName(module),
|
||||||
|
description: getModuleDescription(module),
|
||||||
|
icon: getModuleIcon(module),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
69
datahub-web-react/src/app/homeV3/modules/constants.ts
Normal file
69
datahub-web-react/src/app/homeV3/modules/constants.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { IconNames } from '@components';
|
||||||
|
|
||||||
|
import { ModuleInfo } from '@app/homeV3/modules/types';
|
||||||
|
|
||||||
|
import { DataHubPageModuleType } from '@types';
|
||||||
|
|
||||||
|
// TODO: remove these description once descriptions in modules are implemented
|
||||||
|
export const MODULE_TYPE_TO_DESCRIPTION: Map<DataHubPageModuleType, string> = new Map([
|
||||||
|
[DataHubPageModuleType.AssetCollection, 'A curated list of assets of your choosing'],
|
||||||
|
[DataHubPageModuleType.Domains, 'Most used domains in your organization'],
|
||||||
|
[DataHubPageModuleType.Hierarchy, 'Top down view of assets'],
|
||||||
|
[DataHubPageModuleType.Link, 'Choose links that are important'],
|
||||||
|
[DataHubPageModuleType.OwnedAssets, 'Assets the current user owns'],
|
||||||
|
[DataHubPageModuleType.RichText, 'Pin docs for your DataHub users'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const MODULE_TYPE_TO_ICON: Map<DataHubPageModuleType, IconNames> = new Map([
|
||||||
|
[DataHubPageModuleType.AssetCollection, 'Stack'],
|
||||||
|
[DataHubPageModuleType.Domains, 'Globe'],
|
||||||
|
[DataHubPageModuleType.Hierarchy, 'SortAscending'],
|
||||||
|
[DataHubPageModuleType.Link, 'LinkSimple'],
|
||||||
|
[DataHubPageModuleType.OwnedAssets, 'Database'],
|
||||||
|
[DataHubPageModuleType.RichText, 'TextT'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const DEFAULT_MODULE_ICON = 'Database';
|
||||||
|
|
||||||
|
export const DEFAULT_MODULE_YOUR_ASSETS: ModuleInfo = {
|
||||||
|
type: DataHubPageModuleType.OwnedAssets,
|
||||||
|
name: 'Your Assets',
|
||||||
|
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.OwnedAssets),
|
||||||
|
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.OwnedAssets) ?? DEFAULT_MODULE_ICON,
|
||||||
|
key: 'default_module_your_assets',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MODULE_TOP_DOMAINS: ModuleInfo = {
|
||||||
|
type: DataHubPageModuleType.Domains,
|
||||||
|
name: 'Domains',
|
||||||
|
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.Domains),
|
||||||
|
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.Domains) ?? DEFAULT_MODULE_ICON,
|
||||||
|
key: 'default_module_top_domains',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MODULE_LINK: ModuleInfo = {
|
||||||
|
type: DataHubPageModuleType.Link,
|
||||||
|
name: 'Quick Link',
|
||||||
|
description: MODULE_TYPE_TO_DESCRIPTION.get(DataHubPageModuleType.Link),
|
||||||
|
icon: MODULE_TYPE_TO_ICON.get(DataHubPageModuleType.Link) ?? DEFAULT_MODULE_ICON,
|
||||||
|
key: 'default_module_quick_link',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MODULES: ModuleInfo[] = [
|
||||||
|
DEFAULT_MODULE_YOUR_ASSETS,
|
||||||
|
DEFAULT_MODULE_TOP_DOMAINS,
|
||||||
|
// Links isn't supported yet
|
||||||
|
// DEFAULT_MODULE_LINK,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||||
|
DataHubPageModuleType.Link,
|
||||||
|
DataHubPageModuleType.AssetCollection,
|
||||||
|
DataHubPageModuleType.RichText,
|
||||||
|
DataHubPageModuleType.Hierarchy,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES: DataHubPageModuleType[] = [
|
||||||
|
DataHubPageModuleType.Domains,
|
||||||
|
DataHubPageModuleType.OwnedAssets,
|
||||||
|
];
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES,
|
||||||
|
ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES,
|
||||||
|
DEFAULT_MODULES,
|
||||||
|
} from '@app/homeV3/modules/constants';
|
||||||
|
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||||
|
import { convertModuleToModuleInfo } from '@app/homeV3/modules/utils';
|
||||||
|
|
||||||
|
import { DataHubPageModule, DataHubPageModuleType, EntityType, PageModuleScope } from '@types';
|
||||||
|
|
||||||
|
// TODO: Mocked default modules (should be replaced with the real calling of endpoint once it implemented)
|
||||||
|
const MOCKED_ADMIN_CREATED_MODULES: DataHubPageModule[] = [
|
||||||
|
{
|
||||||
|
urn: 'urn:li:dataHubPageModule:link_admin_1',
|
||||||
|
type: EntityType.DatahubPageModule,
|
||||||
|
properties: {
|
||||||
|
created: {
|
||||||
|
time: 1752056099724,
|
||||||
|
},
|
||||||
|
lastModified: {
|
||||||
|
time: 1752056099724,
|
||||||
|
},
|
||||||
|
name: 'Link 1 (example)',
|
||||||
|
type: DataHubPageModuleType.Link,
|
||||||
|
visibility: {
|
||||||
|
scope: PageModuleScope.Global,
|
||||||
|
},
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urn: 'urn:li:dataHubPageModule:link_admin_2',
|
||||||
|
type: EntityType.DatahubPageModule,
|
||||||
|
properties: {
|
||||||
|
created: {
|
||||||
|
time: 1752056099724,
|
||||||
|
},
|
||||||
|
lastModified: {
|
||||||
|
time: 1752056099724,
|
||||||
|
},
|
||||||
|
name: 'Link 2 (example)',
|
||||||
|
type: DataHubPageModuleType.Link,
|
||||||
|
visibility: {
|
||||||
|
scope: PageModuleScope.Global,
|
||||||
|
},
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function useModulesAvailableToAdd(): ModulesAvailableToAdd {
|
||||||
|
// TODO:: here we have to add logic with getting available modules
|
||||||
|
return useMemo(() => {
|
||||||
|
const customModules = DEFAULT_MODULES.filter((module) =>
|
||||||
|
ADD_MODULE_MENU_SECTION_CUSTOM_MODULE_TYPES.includes(module.type),
|
||||||
|
);
|
||||||
|
const customLargeModules = DEFAULT_MODULES.filter((module) =>
|
||||||
|
ADD_MODULE_MENU_SECTION_CUSTOM_LARGE_MODULE_TYPES.includes(module.type),
|
||||||
|
);
|
||||||
|
const adminCreatedModules = MOCKED_ADMIN_CREATED_MODULES.map(convertModuleToModuleInfo);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customModules,
|
||||||
|
customLargeModules,
|
||||||
|
adminCreatedModules,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
18
datahub-web-react/src/app/homeV3/modules/types.ts
Normal file
18
datahub-web-react/src/app/homeV3/modules/types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { IconNames } from '@components';
|
||||||
|
|
||||||
|
import { DataHubPageModuleType } from '@types';
|
||||||
|
|
||||||
|
export type ModuleInfo = {
|
||||||
|
key: string;
|
||||||
|
urn?: string; // Filled in a case of working with an existing module (e.g. admin created modules)
|
||||||
|
type: DataHubPageModuleType;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
icon: IconNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModulesAvailableToAdd = {
|
||||||
|
customModules: ModuleInfo[];
|
||||||
|
customLargeModules: ModuleInfo[];
|
||||||
|
adminCreatedModules: ModuleInfo[];
|
||||||
|
};
|
||||||
34
datahub-web-react/src/app/homeV3/modules/utils.ts
Normal file
34
datahub-web-react/src/app/homeV3/modules/utils.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { IconNames } from '@components';
|
||||||
|
|
||||||
|
import { DEFAULT_MODULE_ICON, MODULE_TYPE_TO_DESCRIPTION, MODULE_TYPE_TO_ICON } from '@app/homeV3/modules/constants';
|
||||||
|
import { ModuleInfo } from '@app/homeV3/modules/types';
|
||||||
|
|
||||||
|
import { DataHubPageModule, DataHubPageModuleType } from '@types';
|
||||||
|
|
||||||
|
export function getModuleType(module: DataHubPageModule): DataHubPageModuleType {
|
||||||
|
return module.properties.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModuleIcon(module: DataHubPageModule): IconNames {
|
||||||
|
return MODULE_TYPE_TO_ICON.get(getModuleType(module)) ?? DEFAULT_MODULE_ICON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModuleName(module: DataHubPageModule): string {
|
||||||
|
return module.properties.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModuleDescription(module: DataHubPageModule): string | undefined {
|
||||||
|
// TODO: implement getting of the correct description
|
||||||
|
return MODULE_TYPE_TO_DESCRIPTION.get(getModuleType(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertModuleToModuleInfo(module: DataHubPageModule): ModuleInfo {
|
||||||
|
return {
|
||||||
|
urn: module.urn,
|
||||||
|
key: module.urn,
|
||||||
|
type: getModuleType(module),
|
||||||
|
name: getModuleName(module),
|
||||||
|
description: getModuleDescription(module),
|
||||||
|
icon: getModuleIcon(module),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -51,6 +51,7 @@ export const ContentDiv = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledIcon = styled(Icon)`
|
export const StyledIcon = styled(Icon)`
|
||||||
|
|||||||
59
datahub-web-react/src/app/homeV3/template/Template.tsx
Normal file
59
datahub-web-react/src/app/homeV3/template/Template.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { spacing } from '@components';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import useModulesAvailableToAdd from '@app/homeV3/modules/hooks/useModulesAvailableToAdd';
|
||||||
|
import AddModuleButton from '@app/homeV3/template/components/AddModuleButton';
|
||||||
|
import { AddModuleHandlerInput } from '@app/homeV3/template/types';
|
||||||
|
import TemplateRow from '@app/homeV3/templateRow/TemplateRow';
|
||||||
|
|
||||||
|
import { DataHubPageTemplate } from '@types';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${spacing.md};
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Additional margin to have width of content excluding side buttons
|
||||||
|
const StyledAddModulesButton = styled(AddModuleButton)<{ $hasRows?: boolean }>`
|
||||||
|
${(props) => props.$hasRows && 'margin: 0 48px;'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
template: DataHubPageTemplate | null | undefined;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Template({ template, className }: Props) {
|
||||||
|
const hasRows = !!template?.properties?.rows?.length;
|
||||||
|
const onAddModule = useCallback((input: AddModuleHandlerInput) => {
|
||||||
|
// TODO: implement the real handler
|
||||||
|
console.log('onAddModule handled with input', input);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const modulesAvailableToAdd = useModulesAvailableToAdd();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper className={className}>
|
||||||
|
{template?.properties?.rows.map((row, i) => {
|
||||||
|
const key = `templateRow-${i}`;
|
||||||
|
return (
|
||||||
|
<TemplateRow
|
||||||
|
key={key}
|
||||||
|
row={row}
|
||||||
|
rowIndex={i}
|
||||||
|
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||||
|
onAddModule={onAddModule}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<StyledAddModulesButton
|
||||||
|
orientation="horizontal"
|
||||||
|
$hasRows={hasRows}
|
||||||
|
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||||
|
onAddModule={onAddModule}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import { Button, Dropdown, Icon, colors } from '@components';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { ModuleInfo, ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||||
|
import useAddModuleMenu from '@app/homeV3/template/components/addModuleMenu/useAddModuleMenu';
|
||||||
|
import { AddModuleHandlerInput, RowSide } from '@app/homeV3/template/types';
|
||||||
|
|
||||||
|
type AddModuleButtonOrientation = 'vertical' | 'horizontal';
|
||||||
|
|
||||||
|
const Wrapper = styled.div``;
|
||||||
|
|
||||||
|
const StyledDropdownContainer = styled.div`
|
||||||
|
max-width: 330px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)<{ $orientation: AddModuleButtonOrientation; $opened?: boolean }>`
|
||||||
|
${(props) =>
|
||||||
|
props.$orientation === 'vertical'
|
||||||
|
? `
|
||||||
|
height: 100%;
|
||||||
|
width: 32px;
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
width: 32px;
|
||||||
|
width: 100%;
|
||||||
|
`}
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
background: ${colors.gray[1600]};
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
background: ${colors.gray[1600]};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledVisibleOnHoverButton = styled(StyledButton)`
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
${Wrapper}:hover & {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orientation: AddModuleButtonOrientation;
|
||||||
|
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||||
|
onAddModule?: (input: AddModuleHandlerInput) => void;
|
||||||
|
className?: string;
|
||||||
|
rowIndex?: number;
|
||||||
|
rowSide?: RowSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddModuleButton({
|
||||||
|
orientation,
|
||||||
|
modulesAvailableToAdd,
|
||||||
|
onAddModule,
|
||||||
|
className,
|
||||||
|
rowIndex,
|
||||||
|
rowSide,
|
||||||
|
}: Props) {
|
||||||
|
const [isOpened, setIsOpened] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const ButtonComponent = useMemo(() => (isOpened ? StyledButton : StyledVisibleOnHoverButton), [isOpened]);
|
||||||
|
|
||||||
|
const onAddModuleHandler = useCallback(
|
||||||
|
(module: ModuleInfo) => {
|
||||||
|
setIsOpened(false);
|
||||||
|
onAddModule?.({
|
||||||
|
module,
|
||||||
|
rowIndex,
|
||||||
|
rowSide,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onAddModule, rowIndex, rowSide],
|
||||||
|
);
|
||||||
|
|
||||||
|
const menu = useAddModuleMenu(modulesAvailableToAdd, onAddModuleHandler);
|
||||||
|
|
||||||
|
const onClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
// FYI: Antd can open dropdown in the cursor's position only for contextMenu trigger
|
||||||
|
// we handle left click and emit contextmenu event instead to open the dropdown in the cursor's position
|
||||||
|
const event = new MouseEvent('contextmenu', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
clientX: e.clientX,
|
||||||
|
clientY: e.clientY,
|
||||||
|
});
|
||||||
|
event.target?.dispatchEvent(event);
|
||||||
|
|
||||||
|
setIsOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper className={className}>
|
||||||
|
<Dropdown
|
||||||
|
open={isOpened}
|
||||||
|
trigger={['click', 'contextMenu']}
|
||||||
|
onOpenChange={(open) => setIsOpened(open)}
|
||||||
|
dropdownRender={(originNode) => <StyledDropdownContainer>{originNode}</StyledDropdownContainer>}
|
||||||
|
menu={menu}
|
||||||
|
resetDefaultMenuStyles
|
||||||
|
>
|
||||||
|
<ButtonComponent $orientation={orientation} color="gray" variant="text" size="xs" onClick={onClick}>
|
||||||
|
<Icon icon="Plus" source="phosphor" color="primary" />
|
||||||
|
</ButtonComponent>
|
||||||
|
</Dropdown>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,124 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type { ModuleInfo, ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||||
|
import useAddModuleMenu from '@app/homeV3/template/components/addModuleMenu/useAddModuleMenu';
|
||||||
|
|
||||||
|
import { DataHubPageModuleType } from '@types';
|
||||||
|
|
||||||
|
// Mock components that are rendered inside the menu items
|
||||||
|
vi.mock('@app/homeV3/template/components/addModuleMenu/components/GroupItem', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ title }: { title: string }) => <div data-testid="group-item">{title}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@app/homeV3/template/components/addModuleMenu/components/ModuleMenuItem', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ module }: { module: ModuleInfo }) => <div data-testid={`module-${module.key}`}>{module.name}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@app/homeV3/template/components/addModuleMenu/components/MenuItem', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: ({ title }: { title: string }) => <div data-testid="menu-item">{title}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useAddModuleMenu', () => {
|
||||||
|
const mockOnClick = vi.fn();
|
||||||
|
|
||||||
|
const modulesAvailableToAdd: ModulesAvailableToAdd = {
|
||||||
|
customModules: [
|
||||||
|
{ key: 'custom1', name: 'Custom 1', icon: 'Edit', type: DataHubPageModuleType.Link },
|
||||||
|
{ key: 'custom2', name: 'Custom 2', icon: 'Setting', type: DataHubPageModuleType.Link },
|
||||||
|
],
|
||||||
|
customLargeModules: [{ key: 'large1', name: 'Large 1', icon: 'BarChart', type: DataHubPageModuleType.Domains }],
|
||||||
|
adminCreatedModules: [
|
||||||
|
{ key: 'admin1', name: 'Admin Widget 1', icon: 'Database', type: DataHubPageModuleType.Link },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty items when no modules are available', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAddModuleMenu(
|
||||||
|
{
|
||||||
|
customModules: [],
|
||||||
|
customLargeModules: [],
|
||||||
|
adminCreatedModules: [],
|
||||||
|
},
|
||||||
|
mockOnClick,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.items).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate correct items for customModules group', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAddModuleMenu(
|
||||||
|
{
|
||||||
|
...modulesAvailableToAdd,
|
||||||
|
customLargeModules: [],
|
||||||
|
adminCreatedModules: [],
|
||||||
|
},
|
||||||
|
mockOnClick,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { items } = result.current;
|
||||||
|
expect(items).toHaveLength(1);
|
||||||
|
expect(items?.[0]).toHaveProperty('key', 'customModulesGroup');
|
||||||
|
// @ts-expect-error SubMenuItem should have children
|
||||||
|
expect(items?.[0]?.children).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate correct items for customLargeModules group', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAddModuleMenu(
|
||||||
|
{
|
||||||
|
...modulesAvailableToAdd,
|
||||||
|
customModules: [],
|
||||||
|
adminCreatedModules: [],
|
||||||
|
},
|
||||||
|
mockOnClick,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { items } = result.current;
|
||||||
|
expect(items).toHaveLength(1);
|
||||||
|
expect(items?.[0]).toHaveProperty('key', 'customLargeModulesGroup');
|
||||||
|
// @ts-expect-error SubMenuItem should have children
|
||||||
|
expect(items?.[0]?.children).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate correct items for adminCreatedModules group with expandIcon and popupClassName', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAddModuleMenu(
|
||||||
|
{
|
||||||
|
...modulesAvailableToAdd,
|
||||||
|
customModules: [],
|
||||||
|
customLargeModules: [],
|
||||||
|
},
|
||||||
|
mockOnClick,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { items } = result.current;
|
||||||
|
expect(items).toHaveLength(1);
|
||||||
|
expect(items?.[0]).toHaveProperty('key', 'adminCreatedModulesGroup');
|
||||||
|
// @ts-expect-error SubMenuItem should have children
|
||||||
|
expect(items?.[0]?.children).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onClick handler when a module item is clicked', () => {
|
||||||
|
const { result } = renderHook(() => useAddModuleMenu(modulesAvailableToAdd, mockOnClick));
|
||||||
|
|
||||||
|
// @ts-expect-error SubMenuItem should have children
|
||||||
|
const moduleItem = result.current?.items?.[0]?.children?.[0];
|
||||||
|
moduleItem.onClick?.({} as any); // simulate click
|
||||||
|
|
||||||
|
expect(mockOnClick).toHaveBeenCalledWith(modulesAvailableToAdd.customModules[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Text } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GroupItem({ title }: Props) {
|
||||||
|
return (
|
||||||
|
<Text color="gray" weight="bold">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { Icon, IconNames, Text } from '@components';
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import spacing from '@components/theme/foundations/spacing';
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: ${spacing.xsm};
|
||||||
|
padding: ${spacing.xsm};
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-wrap: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SpaceFiller = styled.div`
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: IconNames;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
hasChildren?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MenuItem({ icon, title, description, hasChildren }: Props) {
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<IconWrapper>
|
||||||
|
<Icon icon={icon} source="phosphor" color="gray" size="2xl" />
|
||||||
|
</IconWrapper>
|
||||||
|
|
||||||
|
<Container>
|
||||||
|
<Text weight="semiBold">{title}</Text>
|
||||||
|
{description && (
|
||||||
|
<Text color="gray" colorLevel={1700} size="sm">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<SpaceFiller />
|
||||||
|
|
||||||
|
{hasChildren && <Icon icon="CaretRight" source="phosphor" color="gray" size="lg" />}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ModuleInfo } from '@app/homeV3/modules/types';
|
||||||
|
import MenuItem from '@app/homeV3/template/components/addModuleMenu/components/MenuItem';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
module: ModuleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModuleMenuItem({ module }: Props) {
|
||||||
|
return <MenuItem description={module.description} title={module.name} icon={module.icon} />;
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { MenuProps } from 'antd';
|
||||||
|
import { ItemType } from 'antd/lib/menu/hooks/useItems';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { RESET_DROPDOWN_MENU_STYLES_CLASSNAME } from '@components/components/Dropdown/constants';
|
||||||
|
|
||||||
|
import { ModuleInfo, ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||||
|
import GroupItem from '@app/homeV3/template/components/addModuleMenu/components/GroupItem';
|
||||||
|
import MenuItem from '@app/homeV3/template/components/addModuleMenu/components/MenuItem';
|
||||||
|
import ModuleMenuItem from '@app/homeV3/template/components/addModuleMenu/components/ModuleMenuItem';
|
||||||
|
|
||||||
|
export default function useAddModuleMenu(
|
||||||
|
modulesAvailableToAdd: ModulesAvailableToAdd,
|
||||||
|
onClick?: (module: ModuleInfo) => void,
|
||||||
|
): MenuProps {
|
||||||
|
const convertModule = useCallback(
|
||||||
|
(module: ModuleInfo): ItemType => ({
|
||||||
|
title: module.name,
|
||||||
|
key: module.key,
|
||||||
|
label: <ModuleMenuItem module={module} />,
|
||||||
|
onClick: () => onClick?.(module),
|
||||||
|
}),
|
||||||
|
[onClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const items: MenuProps['items'] = [];
|
||||||
|
|
||||||
|
if (modulesAvailableToAdd.customModules.length) {
|
||||||
|
items.push({
|
||||||
|
key: 'customModulesGroup',
|
||||||
|
label: <GroupItem title="Custom" />,
|
||||||
|
type: 'group',
|
||||||
|
children: modulesAvailableToAdd.customModules.map(convertModule),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modulesAvailableToAdd.customLargeModules.length) {
|
||||||
|
items.push({
|
||||||
|
key: 'customLargeModulesGroup',
|
||||||
|
label: <GroupItem title="Custom Large" />,
|
||||||
|
type: 'group',
|
||||||
|
children: modulesAvailableToAdd.customLargeModules.map(convertModule),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modulesAvailableToAdd.adminCreatedModules.length) {
|
||||||
|
items.push({
|
||||||
|
key: 'adminCreatedModulesGroup',
|
||||||
|
title: 'Admin Created Widgets',
|
||||||
|
label: (
|
||||||
|
<MenuItem
|
||||||
|
icon="Database"
|
||||||
|
title="Admin Created Widgets"
|
||||||
|
description="Your organizations data products"
|
||||||
|
hasChildren
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
expandIcon: <></>, // hide the default expand icon
|
||||||
|
popupClassName: RESET_DROPDOWN_MENU_STYLES_CLASSNAME, // reset styles of submenu
|
||||||
|
children: modulesAvailableToAdd.adminCreatedModules.map(convertModule),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items };
|
||||||
|
}, [modulesAvailableToAdd, convertModule]);
|
||||||
|
}
|
||||||
10
datahub-web-react/src/app/homeV3/template/types.ts
Normal file
10
datahub-web-react/src/app/homeV3/template/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ModuleInfo } from '@app/homeV3/modules/types';
|
||||||
|
|
||||||
|
export type RowSide = 'left' | 'right';
|
||||||
|
|
||||||
|
export interface AddModuleHandlerInput {
|
||||||
|
module: ModuleInfo;
|
||||||
|
// When these fields are empty it means adding a module to the new row
|
||||||
|
rowIndex?: number;
|
||||||
|
rowSide?: RowSide;
|
||||||
|
}
|
||||||
@ -2,8 +2,11 @@ import React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import Module from '@app/homeV3/module/Module';
|
import Module from '@app/homeV3/module/Module';
|
||||||
|
import { ModulesAvailableToAdd } from '@app/homeV3/modules/types';
|
||||||
|
import AddModuleButton from '@app/homeV3/template/components/AddModuleButton';
|
||||||
|
import { AddModuleHandlerInput } from '@app/homeV3/template/types';
|
||||||
|
|
||||||
import { PageTemplateRowFragment } from '@graphql/template.generated';
|
import { DataHubPageTemplateRow } from '@types';
|
||||||
|
|
||||||
const RowWrapper = styled.div`
|
const RowWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -13,15 +16,34 @@ const RowWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
row: PageTemplateRowFragment;
|
row: DataHubPageTemplateRow;
|
||||||
|
onAddModule?: (input: AddModuleHandlerInput) => void;
|
||||||
|
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||||
|
rowIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TemplateRow({ row }: Props) {
|
export default function TemplateRow({ row, onAddModule, modulesAvailableToAdd, rowIndex }: Props) {
|
||||||
return (
|
return (
|
||||||
<RowWrapper>
|
<RowWrapper>
|
||||||
|
<AddModuleButton
|
||||||
|
orientation="vertical"
|
||||||
|
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||||
|
onAddModule={onAddModule}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
rowSide="left"
|
||||||
|
/>
|
||||||
|
|
||||||
{row.modules.map((module) => (
|
{row.modules.map((module) => (
|
||||||
<Module key={module.urn} module={module} />
|
<Module key={module.urn} module={module} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<AddModuleButton
|
||||||
|
orientation="vertical"
|
||||||
|
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||||
|
onAddModule={onAddModule}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
rowSide="right"
|
||||||
|
/>
|
||||||
</RowWrapper>
|
</RowWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,12 @@ fragment pageTemplateFields on DataHubPageTemplate {
|
|||||||
visibility {
|
visibility {
|
||||||
scope
|
scope
|
||||||
}
|
}
|
||||||
|
created {
|
||||||
|
time
|
||||||
|
}
|
||||||
|
lastModified {
|
||||||
|
time
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,8 +32,19 @@ fragment PageModule on DataHubPageModule {
|
|||||||
properties {
|
properties {
|
||||||
name
|
name
|
||||||
type
|
type
|
||||||
|
created {
|
||||||
|
time
|
||||||
|
}
|
||||||
|
lastModified {
|
||||||
|
time
|
||||||
|
}
|
||||||
visibility {
|
visibility {
|
||||||
scope
|
scope
|
||||||
}
|
}
|
||||||
|
params {
|
||||||
|
richTextParams {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user