mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-25 16:05:11 +00:00
feat(customHomePage): adjust rows component and add wrapping (#14025)
This commit is contained in:
parent
7c41e6d4a7
commit
58da8a8857
@ -6,6 +6,7 @@ const ModuleContainer = styled.div<{ $height: string }>`
|
||||
border: ${borders['1px']} ${colors.gray[100]};
|
||||
border-radius: ${radius.lg};
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
|
||||
height: ${(props) => props.$height};
|
||||
box-shadow:
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { spacing } from '@components';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } 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 { wrapRows } from '@app/homeV3/templateRow/utils';
|
||||
|
||||
import { DataHubPageTemplate } from '@types';
|
||||
|
||||
@ -26,7 +27,10 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function Template({ template, className }: Props) {
|
||||
const hasRows = !!template?.properties?.rows?.length;
|
||||
const rows = useMemo(() => template?.properties?.rows ?? [], [template?.properties?.rows]);
|
||||
const hasRows = useMemo(() => !!rows.length, [rows.length]);
|
||||
const wrappedRows = useMemo(() => wrapRows(rows), [rows]);
|
||||
|
||||
const onAddModule = useCallback((input: AddModuleHandlerInput) => {
|
||||
// TODO: implement the real handler
|
||||
console.log('onAddModule handled with input', input);
|
||||
@ -36,13 +40,14 @@ export default function Template({ template, className }: Props) {
|
||||
|
||||
return (
|
||||
<Wrapper className={className}>
|
||||
{template?.properties?.rows.map((row, i) => {
|
||||
{wrappedRows.map((row, i) => {
|
||||
const key = `templateRow-${i}`;
|
||||
return (
|
||||
<TemplateRow
|
||||
key={key}
|
||||
row={row}
|
||||
rowIndex={i}
|
||||
originRowIndex={row.originRowIndex}
|
||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||
onAddModule={onAddModule}
|
||||
/>
|
||||
|
||||
@ -47,6 +47,7 @@ interface Props {
|
||||
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||
onAddModule?: (input: AddModuleHandlerInput) => void;
|
||||
className?: string;
|
||||
originRowIndex?: number;
|
||||
rowIndex?: number;
|
||||
rowSide?: RowSide;
|
||||
}
|
||||
@ -56,6 +57,7 @@ export default function AddModuleButton({
|
||||
modulesAvailableToAdd,
|
||||
onAddModule,
|
||||
className,
|
||||
originRowIndex,
|
||||
rowIndex,
|
||||
rowSide,
|
||||
}: Props) {
|
||||
@ -68,11 +70,12 @@ export default function AddModuleButton({
|
||||
setIsOpened(false);
|
||||
onAddModule?.({
|
||||
module,
|
||||
originRowIndex,
|
||||
rowIndex,
|
||||
rowSide,
|
||||
});
|
||||
},
|
||||
[onAddModule, rowIndex, rowSide],
|
||||
[onAddModule, originRowIndex, rowIndex, rowSide],
|
||||
);
|
||||
|
||||
const menu = useAddModuleMenu(modulesAvailableToAdd, onAddModuleHandler);
|
||||
|
||||
@ -4,7 +4,9 @@ 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;
|
||||
originRowIndex?: number; // Row index before wrapping
|
||||
rowIndex?: number; // Row index after wrapping
|
||||
rowSide?: RowSide;
|
||||
}
|
||||
|
||||
@ -20,15 +20,17 @@ interface Props {
|
||||
onAddModule?: (input: AddModuleHandlerInput) => void;
|
||||
modulesAvailableToAdd: ModulesAvailableToAdd;
|
||||
rowIndex: number;
|
||||
originRowIndex: number;
|
||||
}
|
||||
|
||||
export default function TemplateRow({ row, onAddModule, modulesAvailableToAdd, rowIndex }: Props) {
|
||||
export default function TemplateRow({ row, onAddModule, modulesAvailableToAdd, rowIndex, originRowIndex }: Props) {
|
||||
return (
|
||||
<RowWrapper>
|
||||
<AddModuleButton
|
||||
orientation="vertical"
|
||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||
onAddModule={onAddModule}
|
||||
originRowIndex={originRowIndex}
|
||||
rowIndex={rowIndex}
|
||||
rowSide="left"
|
||||
/>
|
||||
@ -41,6 +43,7 @@ export default function TemplateRow({ row, onAddModule, modulesAvailableToAdd, r
|
||||
orientation="vertical"
|
||||
modulesAvailableToAdd={modulesAvailableToAdd}
|
||||
onAddModule={onAddModule}
|
||||
originRowIndex={originRowIndex}
|
||||
rowIndex={rowIndex}
|
||||
rowSide="right"
|
||||
/>
|
||||
|
||||
@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { WrappedRow } from '@app/homeV3/templateRow/types';
|
||||
import { wrapRows } from '@app/homeV3/templateRow/utils';
|
||||
|
||||
import { DataHubPageModuleType, DataHubPageTemplateRow, EntityType, PageModuleScope } from '@types';
|
||||
|
||||
const MOCKED_TIMESTAMP = 1752056099724;
|
||||
|
||||
describe('wrapRows', () => {
|
||||
const makeRow = (moduleCount: number): DataHubPageTemplateRow => ({
|
||||
modules: Array.from({ length: moduleCount }).map((_, i) => ({
|
||||
urn: `urn:li:module:${i}`,
|
||||
type: EntityType.DatahubPageModule,
|
||||
properties: {
|
||||
name: `Module ${i}`,
|
||||
type: DataHubPageModuleType.OwnedAssets,
|
||||
visibility: {
|
||||
scope: PageModuleScope.Global,
|
||||
},
|
||||
created: {
|
||||
time: MOCKED_TIMESTAMP,
|
||||
},
|
||||
lastModified: {
|
||||
time: MOCKED_TIMESTAMP,
|
||||
},
|
||||
params: {},
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
it('should split modules into chunks of default size (3)', () => {
|
||||
const rows: DataHubPageTemplateRow[] = [
|
||||
makeRow(7), // 7 modules → [3, 3, 1]
|
||||
];
|
||||
|
||||
const result = wrapRows(rows);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].modules).toHaveLength(3);
|
||||
expect(result[1].modules).toHaveLength(3);
|
||||
expect(result[2].modules).toHaveLength(1);
|
||||
expect(result[0].originRowIndex).toBe(0);
|
||||
expect(result[0].rowIndex).toBe(0);
|
||||
expect(result[1].rowIndex).toBe(1);
|
||||
expect(result[2].rowIndex).toBe(2);
|
||||
});
|
||||
|
||||
it('should respect custom chunk size', () => {
|
||||
const rows: DataHubPageTemplateRow[] = [makeRow(5)];
|
||||
|
||||
const result = wrapRows(rows, 2); // chunk size = 2 → [2, 2, 1]
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].modules).toHaveLength(2);
|
||||
expect(result[1].modules).toHaveLength(2);
|
||||
expect(result[2].modules).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle multiple rows correctly', () => {
|
||||
const rows: DataHubPageTemplateRow[] = [
|
||||
makeRow(4), // → [3, 1]
|
||||
makeRow(5), // → [3, 2]
|
||||
];
|
||||
|
||||
const result = wrapRows(rows);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
|
||||
// First row
|
||||
expect(result[0].originRowIndex).toBe(0);
|
||||
expect(result[0].rowIndex).toBe(0);
|
||||
|
||||
expect(result[1].originRowIndex).toBe(0);
|
||||
expect(result[1].rowIndex).toBe(1);
|
||||
|
||||
// Second row
|
||||
expect(result[2].originRowIndex).toBe(1);
|
||||
expect(result[2].rowIndex).toBe(2);
|
||||
|
||||
expect(result[3].originRowIndex).toBe(1);
|
||||
expect(result[3].rowIndex).toBe(3);
|
||||
});
|
||||
|
||||
it('should return empty array if no rows provided', () => {
|
||||
const result = wrapRows([]);
|
||||
expect(result).toEqual<WrappedRow[]>([]);
|
||||
});
|
||||
|
||||
it('should return empty array if all rows have no modules', () => {
|
||||
const rows: DataHubPageTemplateRow[] = [{ modules: [] }, { modules: [] }];
|
||||
|
||||
const result = wrapRows(rows);
|
||||
expect(result).toEqual<WrappedRow[]>([]);
|
||||
});
|
||||
|
||||
it('should handle exact multiples of chunk size', () => {
|
||||
const rows: DataHubPageTemplateRow[] = [
|
||||
makeRow(6), // 6 modules → [3, 3]
|
||||
];
|
||||
|
||||
const result = wrapRows(rows);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].modules).toHaveLength(3);
|
||||
expect(result[1].modules).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
6
datahub-web-react/src/app/homeV3/templateRow/types.ts
Normal file
6
datahub-web-react/src/app/homeV3/templateRow/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { DataHubPageTemplateRow } from '@types';
|
||||
|
||||
export interface WrappedRow extends DataHubPageTemplateRow {
|
||||
originRowIndex: number;
|
||||
rowIndex: number;
|
||||
}
|
||||
25
datahub-web-react/src/app/homeV3/templateRow/utils.ts
Normal file
25
datahub-web-react/src/app/homeV3/templateRow/utils.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { WrappedRow } from '@app/homeV3/templateRow/types';
|
||||
|
||||
import { DataHubPageTemplateRow } from '@types';
|
||||
|
||||
const DEFAULT_ROW_MAX_SIZE = 3;
|
||||
|
||||
export function wrapRows(rows: DataHubPageTemplateRow[], chunkSize = DEFAULT_ROW_MAX_SIZE): WrappedRow[] {
|
||||
const result: WrappedRow[] = [];
|
||||
let globalRowIndex = 0;
|
||||
|
||||
rows.forEach((row, originRowIndex) => {
|
||||
const { modules } = row;
|
||||
|
||||
for (let i = 0; i < modules.length; i += chunkSize) {
|
||||
const chunk = modules.slice(i, i + chunkSize);
|
||||
result.push({
|
||||
originRowIndex,
|
||||
rowIndex: globalRowIndex++,
|
||||
modules: chunk,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Tooltip } from '@components';
|
||||
import { Tooltip, zIndices } from '@components';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
@ -98,7 +98,7 @@ const EntityHeader: React.FC<EntityHeaderProps> = ({
|
||||
<EntityTitleContainer>
|
||||
<StyledLink to={`${url}/`} {...linkProps}>
|
||||
{previewType === PreviewType.HOVER_CARD ? (
|
||||
<Tooltip title={name}>
|
||||
<Tooltip title={name} zIndex={zIndices.tooltip}>
|
||||
<CardEntityTitle onClick={onClick} $titleSizePx={titleSizePx} data-testid="entity-title">
|
||||
{name || urn}
|
||||
</CardEntityTitle>
|
||||
|
||||
@ -36,7 +36,7 @@ const DisplayNameWithHover = styled(DisplayName)<{ $decorationColor?: string }>`
|
||||
`;
|
||||
|
||||
const DisplayNameWrapper = styled.div`
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
@ -131,6 +131,7 @@ export default function AutoCompleteEntityItem({
|
||||
color={variantProps?.nameColor}
|
||||
colorLevel={variantProps?.nameColorLevel}
|
||||
weight={variantProps?.nameWeight}
|
||||
showNameTooltipIfTruncated
|
||||
/>
|
||||
)}
|
||||
</DisplayNameWrapper>
|
||||
|
||||
@ -25,15 +25,29 @@ interface Props {
|
||||
weight?: FontWeightOptions;
|
||||
fontSize?: FontSizeOptions;
|
||||
className?: string;
|
||||
showNameTooltipIfTruncated?: boolean;
|
||||
}
|
||||
|
||||
export default function DisplayName({ displayName, highlight, color, colorLevel, weight, fontSize, className }: Props) {
|
||||
export default function DisplayName({
|
||||
displayName,
|
||||
highlight,
|
||||
color,
|
||||
colorLevel,
|
||||
weight,
|
||||
fontSize,
|
||||
className,
|
||||
showNameTooltipIfTruncated,
|
||||
}: Props) {
|
||||
const { measuredRef, isHorizontallyTruncated } = useMeasureIfTrancated();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
zIndex={zIndices.popover}
|
||||
content={isHorizontallyTruncated ? <PopoverWrapper>{displayName}</PopoverWrapper> : undefined}
|
||||
content={
|
||||
showNameTooltipIfTruncated && isHorizontallyTruncated ? (
|
||||
<PopoverWrapper>{displayName}</PopoverWrapper>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<EntityTitleContainer ref={measuredRef} className={className}>
|
||||
<MatchText
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user