diff --git a/frontend/src/components/DropDown/DropDown.styles.scss b/frontend/src/components/DropDown/DropDown.styles.scss new file mode 100644 index 0000000000..6042acd07c --- /dev/null +++ b/frontend/src/components/DropDown/DropDown.styles.scss @@ -0,0 +1,7 @@ +.Dropdown-button { + color: #fff; +} + +.Dropdown-icon { + font-size: 1.2rem; +} \ No newline at end of file diff --git a/frontend/src/components/DropDown/DropDown.tsx b/frontend/src/components/DropDown/DropDown.tsx new file mode 100644 index 0000000000..2f45a6a1a7 --- /dev/null +++ b/frontend/src/components/DropDown/DropDown.tsx @@ -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 ( + + + + ); +} + +export default DropDown; diff --git a/frontend/src/components/ResizeTable/DynamicColumnTable.syles.scss b/frontend/src/components/ResizeTable/DynamicColumnTable.syles.scss new file mode 100644 index 0000000000..30bccd87e3 --- /dev/null +++ b/frontend/src/components/ResizeTable/DynamicColumnTable.syles.scss @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/ResizeTable/DynamicColumnTable.tsx b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx new file mode 100644 index 0000000000..93e3673743 --- /dev/null +++ b/frontend/src/components/ResizeTable/DynamicColumnTable.tsx @@ -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( + 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, + ): 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: ( +
+
{column.title?.toString()}
+ c.key === column.key) !== -1} + onChange={onToggleHandler(index)} + /> +
+ ), + key: index, + type: 'checkbox', + })) || []; + + return ( +
+ {dynamicColumns && ( + +
+ ); +} + +DynamicColumnTable.defaultProps = { + onDragColumn: undefined, +}; + +export default memo(DynamicColumnTable); diff --git a/frontend/src/components/ResizeTable/TableComponent/Date.tsx b/frontend/src/components/ResizeTable/TableComponent/Date.tsx new file mode 100644 index 0000000000..d14c2b6b53 --- /dev/null +++ b/frontend/src/components/ResizeTable/TableComponent/Date.tsx @@ -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 - ; + } + + return ( + {timeString} + ); +} + +export default DateComponent; diff --git a/frontend/src/components/ResizeTable/contants.ts b/frontend/src/components/ResizeTable/contants.ts new file mode 100644 index 0000000000..0944f9aa12 --- /dev/null +++ b/frontend/src/components/ResizeTable/contants.ts @@ -0,0 +1,11 @@ +export const TableDataSource = { + Alert: 'alert', + Dashboard: 'dashboard', +} as const; + +export const DynamicColumnsKey = { + CreatedAt: 'createdAt', + CreatedBy: 'createdBy', + UpdatedAt: 'updatedAt', + UpdatedBy: 'updatedBy', +}; diff --git a/frontend/src/components/ResizeTable/types.ts b/frontend/src/components/ResizeTable/types.ts index 6390a25ba6..c212bfa505 100644 --- a/frontend/src/components/ResizeTable/types.ts +++ b/frontend/src/components/ResizeTable/types.ts @@ -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 { onDragColumn?: (fromIndex: number, toIndex: number) => void; } +export interface DynamicColumnTableProps extends TableProps { + tablesource: typeof TableDataSource[keyof typeof TableDataSource]; + dynamicColumns: TableProps['columns']; + onDragColumn?: (fromIndex: number, toIndex: number) => void; +} + +export type GetVisibleColumnsFunction = ( + props: GetVisibleColumnProps, +) => (ColumnGroupType | ColumnType)[]; + +export type GetVisibleColumnProps = { + tablesource: typeof TableDataSource[keyof typeof TableDataSource]; + dynamicColumns?: ColumnsType; + columnsData?: ColumnsType; +}; + +export type SetVisibleColumnsProps = { + checked: boolean; + index: number; + tablesource: typeof TableDataSource[keyof typeof TableDataSource]; + dynamicColumns?: ColumnsType; +}; diff --git a/frontend/src/components/ResizeTable/unit.ts b/frontend/src/components/ResizeTable/unit.ts new file mode 100644 index 0000000000..0bd4b3bb1f --- /dev/null +++ b/frontend/src/components/ResizeTable/unit.ts @@ -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 = {}; + 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); + } +}; diff --git a/frontend/src/components/TableRenderer/LabelColumn.styles.scss b/frontend/src/components/TableRenderer/LabelColumn.styles.scss new file mode 100644 index 0000000000..617dad5e7b --- /dev/null +++ b/frontend/src/components/TableRenderer/LabelColumn.styles.scss @@ -0,0 +1,9 @@ +.LabelColumn { + .LabelColumn-label-tag { + white-space: normal; + } + +} +.labelColumn-popover { + margin: 0.5rem 0; +} \ No newline at end of file diff --git a/frontend/src/components/TableRenderer/LabelColumn.tsx b/frontend/src/components/TableRenderer/LabelColumn.tsx new file mode 100644 index 0000000000..05bc4eb072 --- /dev/null +++ b/frontend/src/components/TableRenderer/LabelColumn.tsx @@ -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 ( +
+ {newLabels.map( + (label: string): JSX.Element => { + const tooltipTitle = + value && value[label] ? `${label}: ${value[label]}` : label; + return ( + + + {getLabelRenderingValue(label, value && value[label])} + + + ); + }, + )} + {remainingLabels.length > 0 && ( + + {labels.map( + (label: string): JSX.Element => { + const tooltipTitle = + value && value[label] ? `${label}: ${value[label]}` : label; + return ( +
+ + + {getLabelRenderingValue(label, value && value[label])} + + +
+ ); + }, + )} +
+ } + trigger="hover" + > + + +{remainingLabels.length} + + + )} + + ); +} + +LabelColumn.defaultProps = { + value: {}, +}; + +export default LabelColumn; diff --git a/frontend/src/components/TableRenderer/TableRenderer.types.ts b/frontend/src/components/TableRenderer/TableRenderer.types.ts new file mode 100644 index 0000000000..52aa40dd3f --- /dev/null +++ b/frontend/src/components/TableRenderer/TableRenderer.types.ts @@ -0,0 +1,5 @@ +export type LabelColumnProps = { + labels: string[]; + color?: string; + value?: { [key: string]: string }; +}; diff --git a/frontend/src/components/TableRenderer/utils.ts b/frontend/src/components/TableRenderer/utils.ts index 1e3ccc3cd2..ebceffe436 100644 --- a/frontend/src/components/TableRenderer/utils.ts +++ b/frontend/src/components/TableRenderer/utils.ts @@ -16,6 +16,28 @@ export const generatorResizeTableColumns = ({ }; }); +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 { baseColumnOptions: ColumnsType; dynamicColumnOption: { key: string; columnOption: ColumnType }[]; diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index 7dd1052a67..0429174de5 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -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 = [ + { + 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 =>
{value}
, + }, + { + 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 =>
{value}
, + }, + ]; + const columns: ColumnsType = [ { title: 'Status', @@ -178,13 +232,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { } return ( - <> - {withOutSeverityKeys.map((e) => ( - - {e}: {value[e]} - - ))} - + ); }, }, @@ -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 => ( - <> - - - - Edit - - - Clone - - - - + , + + Edit + , + + Clone + , + , + ]} + /> ), }); } @@ -229,7 +287,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { )} - + ); } diff --git a/frontend/src/container/ListAlertRules/styles.ts b/frontend/src/container/ListAlertRules/styles.ts index 67748b21c0..f2d0937950 100644 --- a/frontend/src/container/ListAlertRules/styles.ts +++ b/frontend/src/container/ListAlertRules/styles.ts @@ -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; - } -`; diff --git a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts index 369a0dffa3..b215d205a5 100644 --- a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts +++ b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts @@ -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: {}, diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx deleted file mode 100644 index c96ac1ebf1..0000000000 --- a/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx +++ /dev/null @@ -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 {timeString}; -} - -export default DateComponent; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index 03c0ac9912..c68b0c2617 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -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 ( ); diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index 87f47e54af..bb47b15579 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -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[] = [ + { + 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 =>
{value}
, + }, + { + 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 =>
{value}
, + }, + ]; + const columns = useMemo(() => { const tableColumns: TableColumnProps[] = [ { 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 => , }, ]; @@ -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)} /> -