[Feat]: Dynamic columns in tables (#3809)

* feat: added dropdown in alert list table

* refactor: done with combining actions

* feat: done with label and dynamic table

* feat: dynamic column in table

* chore: show all label on hover

* refactor: create to created timestamp - highlighted action

* refactor: storing the column data in localstorage
This commit is contained in:
Rajat Dabade 2023-10-27 21:09:23 +05:30 committed by GitHub
parent b34eafcab1
commit fc49833c9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 567 additions and 84 deletions

View File

@ -0,0 +1,7 @@
.Dropdown-button {
color: #fff;
}
.Dropdown-icon {
font-size: 1.2rem;
}

View File

@ -0,0 +1,29 @@
import './DropDown.styles.scss';
import { EllipsisOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Space } from 'antd';
function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
const items: MenuProps['items'] = element.map(
(e: JSX.Element, index: number) => ({
label: e,
key: index,
}),
);
return (
<Dropdown menu={{ items }} className="Dropdown-container">
<Button
type="link"
className="Dropdown-button"
onClick={(e): void => e.preventDefault()}
>
<Space>
<EllipsisOutlined className="Dropdown-icon" />
</Space>
</Button>
</Dropdown>
);
}
export default DropDown;

View File

@ -0,0 +1,17 @@
.DynamicColumnTable {
display: flex;
flex-direction: column;
width: 100%;
.dynamicColumnTable-button {
align-self: flex-end;
margin: 10px 0;
}
}
.dynamicColumnsTable-items {
display: flex;
width: 10.625rem;
justify-content: space-between;
align-items: center;
}

View File

@ -0,0 +1,116 @@
/* eslint-disable react/jsx-props-no-spreading */
import './DynamicColumnTable.syles.scss';
import { SettingOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps, Switch } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
import ResizeTable from './ResizeTable';
import { DynamicColumnTableProps } from './types';
import { getVisibleColumns, setVisibleColumns } from './unit';
function DynamicColumnTable({
tablesource,
columns,
dynamicColumns,
onDragColumn,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
columns,
);
useEffect(() => {
const visibleColumns = getVisibleColumns({
tablesource,
columnsData: columns,
dynamicColumns,
});
setColumnsData((prevColumns) =>
prevColumns
? [
...prevColumns.slice(0, prevColumns.length - 1),
...visibleColumns,
prevColumns[prevColumns.length - 1],
]
: undefined,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onToggleHandler = (index: number) => (
checked: boolean,
event: React.MouseEvent<HTMLButtonElement>,
): void => {
event.stopPropagation();
setVisibleColumns({
tablesource,
dynamicColumns,
index,
checked,
});
setColumnsData((prevColumns) => {
if (checked && dynamicColumns) {
return prevColumns
? [
...prevColumns.slice(0, prevColumns.length - 1),
dynamicColumns[index],
prevColumns[prevColumns.length - 1],
]
: undefined;
}
return prevColumns && dynamicColumns
? prevColumns.filter(
(column) => dynamicColumns[index].title !== column.title,
)
: undefined;
});
};
const items: MenuProps['items'] =
dynamicColumns?.map((column, index) => ({
label: (
<div className="dynamicColumnsTable-items">
<div>{column.title?.toString()}</div>
<Switch
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
onChange={onToggleHandler(index)}
/>
</div>
),
key: index,
type: 'checkbox',
})) || [];
return (
<div className="DynamicColumnTable">
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<Button
className="dynamicColumnTable-button"
size="middle"
icon={<SettingOutlined />}
/>
</Dropdown>
)}
<ResizeTable
columns={columnsData}
onDragColumn={onDragColumn}
{...restProps}
/>
</div>
);
}
DynamicColumnTable.defaultProps = {
onDragColumn: undefined,
};
export default memo(DynamicColumnTable);

View File

@ -0,0 +1,23 @@
import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
function DateComponent(
CreatedOrUpdateTime: string | number | Date,
): JSX.Element {
const time = new Date(CreatedOrUpdateTime);
const date = getFormattedDate(time);
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
if (CreatedOrUpdateTime === null) {
return <Typography> - </Typography>;
}
return (
<Typography className="DateComponent-container">{timeString}</Typography>
);
}
export default DateComponent;

View File

@ -0,0 +1,11 @@
export const TableDataSource = {
Alert: 'alert',
Dashboard: 'dashboard',
} as const;
export const DynamicColumnsKey = {
CreatedAt: 'createdAt',
CreatedBy: 'createdBy',
UpdatedAt: 'updatedAt',
UpdatedBy: 'updatedBy',
};

View File

@ -1,6 +1,32 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { TableDataSource } from './contants';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ResizeTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
}
export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
}
export type GetVisibleColumnsFunction = (
props: GetVisibleColumnProps,
) => (ColumnGroupType<any> | ColumnType<any>)[];
export type GetVisibleColumnProps = {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns?: ColumnsType<any>;
columnsData?: ColumnsType;
};
export type SetVisibleColumnsProps = {
checked: boolean;
index: number;
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns?: ColumnsType<any>;
};

View File

@ -0,0 +1,57 @@
import { DynamicColumnsKey } from './contants';
import {
GetVisibleColumnProps,
GetVisibleColumnsFunction,
SetVisibleColumnsProps,
} from './types';
export const getVisibleColumns: GetVisibleColumnsFunction = ({
tablesource,
dynamicColumns,
columnsData,
}: GetVisibleColumnProps) => {
let columnVisibilityData: { [key: string]: boolean };
try {
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
columnVisibilityData = JSON.parse(storedData);
return dynamicColumns.filter((column) => {
if (column.key && !columnsData?.find((c) => c.key === column.key)) {
return columnVisibilityData[column.key];
}
return false;
});
}
const initialColumnVisibility: Record<string, boolean> = {};
Object.values(DynamicColumnsKey).forEach((key) => {
initialColumnVisibility[key] = false;
});
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
} catch (error) {
console.error(error);
}
return [];
};
export const setVisibleColumns = ({
checked,
index,
tablesource,
dynamicColumns,
}: SetVisibleColumnsProps): void => {
try {
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
const columnVisibilityData = JSON.parse(storedData);
const { key } = dynamicColumns[index];
if (key) {
columnVisibilityData[key] = checked;
}
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
}
} catch (error) {
console.error(error);
}
};

View File

@ -0,0 +1,9 @@
.LabelColumn {
.LabelColumn-label-tag {
white-space: normal;
}
}
.labelColumn-popover {
margin: 0.5rem 0;
}

View File

@ -0,0 +1,67 @@
import './LabelColumn.styles.scss';
import { Popover, Tag, Tooltip } from 'antd';
import { popupContainer } from 'utils/selectPopupContainer';
import { LabelColumnProps } from './TableRenderer.types';
import { getLabelRenderingValue } from './utils';
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
const newLabels = labels.length > 3 ? labels.slice(0, 3) : labels;
const remainingLabels = labels.length > 3 ? labels.slice(3) : [];
return (
<div className="LabelColumn">
{newLabels.map(
(label: string): JSX.Element => {
const tooltipTitle =
value && value[label] ? `${label}: ${value[label]}` : label;
return (
<Tooltip title={tooltipTitle} key={label}>
<Tag className="LabelColumn-label-tag" color={color}>
{getLabelRenderingValue(label, value && value[label])}
</Tag>
</Tooltip>
);
},
)}
{remainingLabels.length > 0 && (
<Popover
getPopupContainer={popupContainer}
placement="bottomRight"
showArrow={false}
content={
<div>
{labels.map(
(label: string): JSX.Element => {
const tooltipTitle =
value && value[label] ? `${label}: ${value[label]}` : label;
return (
<div className="labelColumn-popover" key={label}>
<Tooltip title={tooltipTitle}>
<Tag className="LabelColumn-label-tag" color={color}>
{getLabelRenderingValue(label, value && value[label])}
</Tag>
</Tooltip>
</div>
);
},
)}
</div>
}
trigger="hover"
>
<Tag className="LabelColumn-label-tag" color={color}>
+{remainingLabels.length}
</Tag>
</Popover>
)}
</div>
);
}
LabelColumn.defaultProps = {
value: {},
};
export default LabelColumn;

View File

@ -0,0 +1,5 @@
export type LabelColumnProps = {
labels: string[];
color?: string;
value?: { [key: string]: string };
};

View File

@ -16,6 +16,28 @@ export const generatorResizeTableColumns = <T>({
};
});
export const getLabelRenderingValue = (
label: string,
value?: string,
): string => {
const maxLength = 20;
if (label.length > maxLength) {
return `${label.slice(0, maxLength)}...`;
}
if (value) {
const remainingSpace = maxLength - label.length;
let newValue = value;
if (value.length > remainingSpace) {
newValue = `${value.slice(0, remainingSpace)}...`;
}
return `${label}: ${newValue}`;
}
return label;
};
interface GeneratorResizeTableColumnsProp<T> {
baseColumnOptions: ColumnsType<T>;
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];

View File

@ -3,7 +3,14 @@ import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import saveAlertApi from 'api/alerts/save';
import { ResizeTable } from 'components/ResizeTable';
import DropDown from 'components/DropDown/DropDown';
import {
DynamicColumnsKey,
TableDataSource,
} from 'components/ResizeTable/contants';
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import DateComponent from 'components/ResizeTable/TableComponent/Date';
import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
@ -22,7 +29,7 @@ import { GettableAlert } from 'types/api/alerts/get';
import AppReducer from 'types/reducer/app';
import DeleteAlert from './DeleteAlert';
import { Button, ButtonContainer, ColumnButton, StyledTag } from './styles';
import { Button, ButtonContainer, ColumnButton } from './styles';
import Status from './TableComponents/Status';
import ToggleAlertState from './ToggleAlertState';
@ -121,6 +128,53 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}
};
const dynamicColumns: ColumnsType<GettableAlert> = [
{
title: 'Created At',
dataIndex: 'createAt',
width: 80,
key: DynamicColumnsKey.CreatedAt,
align: 'center',
sorter: (a: GettableAlert, b: GettableAlert): number => {
const prev = new Date(a.createAt).getTime();
const next = new Date(b.createAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Created By',
dataIndex: 'createBy',
width: 80,
key: DynamicColumnsKey.CreatedBy,
align: 'center',
render: (value): JSX.Element => <div>{value}</div>,
},
{
title: 'Updated At',
dataIndex: 'updateAt',
width: 80,
key: DynamicColumnsKey.UpdatedAt,
align: 'center',
sorter: (a: GettableAlert, b: GettableAlert): number => {
const prev = new Date(a.updateAt).getTime();
const next = new Date(b.updateAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Updated By',
dataIndex: 'updateBy',
width: 80,
key: DynamicColumnsKey.UpdatedBy,
align: 'center',
render: (value): JSX.Element => <div>{value}</div>,
},
];
const columns: ColumnsType<GettableAlert> = [
{
title: 'Status',
@ -178,13 +232,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}
return (
<>
{withOutSeverityKeys.map((e) => (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
))}
</>
<LabelColumn labels={withOutSeverityKeys} value={value} color="magenta" />
);
},
},
@ -195,20 +243,30 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
title: 'Action',
dataIndex: 'id',
key: 'action',
width: 120,
width: 10,
render: (id: GettableAlert['id'], record): JSX.Element => (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
<ColumnButton onClick={onEditHandler(record)} type="link">
Edit
</ColumnButton>
<ColumnButton onClick={onCloneHandler(record)} type="link">
Clone
</ColumnButton>
<DeleteAlert notifications={notificationsApi} setData={setData} id={id} />
</>
<DropDown
element={[
<ToggleAlertState
key="1"
disabled={record.disabled}
setData={setData}
id={id}
/>,
<ColumnButton key="2" onClick={onEditHandler(record)} type="link">
Edit
</ColumnButton>,
<ColumnButton key="3" onClick={onCloneHandler(record)} type="link">
Clone
</ColumnButton>,
<DeleteAlert
key="4"
notifications={notificationsApi}
setData={setData}
id={id}
/>,
]}
/>
),
});
}
@ -229,7 +287,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
</Button>
)}
</ButtonContainer>
<ResizeTable columns={columns} rowKey="id" dataSource={data} />
<DynamicColumnTable
tablesource={TableDataSource.Alert}
columns={columns}
rowKey="id"
dataSource={data}
dynamicColumns={dynamicColumns}
/>
</>
);
}

View File

@ -1,4 +1,4 @@
import { Button as ButtonComponent, Tag } from 'antd';
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const ButtonContainer = styled.div`
@ -23,9 +23,3 @@ export const ColumnButton = styled(ButtonComponent)`
margin-right: 1.5em;
}
`;
export const StyledTag = styled(Tag)`
&&& {
white-space: normal;
}
`;

View File

@ -10,6 +10,8 @@ describe('executeSearchQueries', () => {
uuid: uuid(),
created_at: '',
updated_at: '',
created_by: '',
updated_by: '',
data: {
title: 'first dashboard',
variables: {},
@ -20,6 +22,8 @@ describe('executeSearchQueries', () => {
uuid: uuid(),
created_at: '',
updated_at: '',
created_by: '',
updated_by: '',
data: {
title: 'second dashboard',
variables: {},
@ -30,6 +34,8 @@ describe('executeSearchQueries', () => {
uuid: uuid(),
created_at: '',
updated_at: '',
created_by: '',
updated_by: '',
data: {
title: 'third dashboard (with special characters +?\\)',
variables: {},

View File

@ -1,17 +0,0 @@
import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
import { Data } from '..';
function DateComponent(lastUpdatedTime: Data['lastUpdatedTime']): JSX.Element {
const time = new Date(lastUpdatedTime);
const date = getFormattedDate(time);
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
return <Typography>{timeString}</Typography>;
}
export default DateComponent;

View File

@ -45,18 +45,30 @@ function DeleteButton({ id }: Data): JSX.Element {
// This is to avoid the type collision
function Wrapper(props: Data): JSX.Element {
const { createdBy, description, id, key, lastUpdatedTime, name, tags } = props;
const {
createdAt,
description,
id,
key,
lastUpdatedTime,
name,
tags,
createdBy,
lastUpdatedBy,
} = props;
return (
<DeleteButton
{...{
createdBy,
createdAt,
description,
id,
key,
lastUpdatedTime,
name,
tags,
createdBy,
lastUpdatedBy,
}}
/>
);

View File

@ -10,7 +10,12 @@ import {
import { ItemType } from 'antd/es/menu/hooks/useItems';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import { ResizeTable } from 'components/ResizeTable';
import {
DynamicColumnsKey,
TableDataSource,
} from 'components/ResizeTable/contants';
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
@ -26,13 +31,11 @@ import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { popupContainer } from 'utils/selectPopupContainer';
import DateComponent from '../../components/ResizeTable/TableComponent/Date';
import ImportJSON from './ImportJSON';
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
import Createdby from './TableComponents/CreatedBy';
import DateComponent from './TableComponents/Date';
import DeleteButton from './TableComponents/DeleteButton';
import Name from './TableComponents/Name';
import Tags from './TableComponents/Tags';
function ListOfAllDashboard(): JSX.Element {
const {
@ -71,48 +74,68 @@ function ListOfAllDashboard(): JSX.Element {
errorMessage: '',
});
const dynamicColumns: TableColumnProps<Data>[] = [
{
title: 'Created At',
dataIndex: 'createdAt',
width: 30,
key: DynamicColumnsKey.CreatedAt,
sorter: (a: Data, b: Data): number => {
console.log({ a });
const prev = new Date(a.createdAt).getTime();
const next = new Date(b.createdAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Created By',
dataIndex: 'createdBy',
width: 30,
key: DynamicColumnsKey.CreatedBy,
render: (value): JSX.Element => <div>{value}</div>,
},
{
title: 'Last Updated Time',
width: 30,
dataIndex: 'lastUpdatedTime',
key: DynamicColumnsKey.UpdatedAt,
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.lastUpdatedTime).getTime();
const next = new Date(b.lastUpdatedTime).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Last Updated By',
dataIndex: 'lastUpdatedBy',
width: 30,
key: DynamicColumnsKey.UpdatedBy,
render: (value): JSX.Element => <div>{value}</div>,
},
];
const columns = useMemo(() => {
const tableColumns: TableColumnProps<Data>[] = [
{
title: 'Name',
dataIndex: 'name',
width: 100,
width: 40,
render: Name,
},
{
title: 'Description',
width: 100,
width: 50,
dataIndex: 'description',
},
{
title: 'Tags (can be multiple)',
dataIndex: 'tags',
width: 80,
render: Tags,
},
{
title: 'Created At',
dataIndex: 'createdBy',
width: 80,
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.createdBy).getTime();
const next = new Date(b.createdBy).getTime();
return prev - next;
},
render: Createdby,
},
{
title: 'Last Updated Time',
width: 90,
dataIndex: 'lastUpdatedTime',
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.lastUpdatedTime).getTime();
const next = new Date(b.lastUpdatedTime).getTime();
return prev - next;
},
render: DateComponent,
width: 50,
render: (value): JSX.Element => <LabelColumn labels={value} />,
},
];
@ -130,13 +153,15 @@ function ListOfAllDashboard(): JSX.Element {
const data: Data[] =
filteredDashboards?.map((e) => ({
createdBy: e.created_at,
createdAt: e.created_at,
description: e.data.description || '',
id: e.uuid,
lastUpdatedTime: e.updated_at,
name: e.data.title,
tags: e.data.tags || [],
key: e.uuid,
createdBy: e.created_by,
lastUpdatedBy: e.updated_by,
refetchDashboardList,
})) || [];
@ -290,7 +315,9 @@ function ListOfAllDashboard(): JSX.Element {
uploadedGrafana={uploadedGrafana}
onModalHandler={(): void => onModalHandler(false)}
/>
<ResizeTable
<DynamicColumnTable
tablesource={TableDataSource.Dashboard}
dynamicColumns={dynamicColumns}
columns={columns}
pagination={{
pageSize: 9,
@ -314,7 +341,9 @@ export interface Data {
description: string;
tags: string[];
createdBy: string;
createdAt: string;
lastUpdatedTime: string;
lastUpdatedBy: string;
id: string;
}

View File

@ -9,6 +9,10 @@ export interface GettableAlert extends AlertDef {
alert: string;
state: string;
disabled: boolean;
createAt: string;
createBy: string;
updateAt: string;
updateBy: string;
}
export type PayloadProps = {

View File

@ -42,6 +42,8 @@ export interface Dashboard {
uuid: string;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
data: DashboardData;
}