diff --git a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx index e0a962f49..bb6027292 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx @@ -1,56 +1,55 @@ import React, { useState, useMemo } from 'react'; -import { Checkbox, Table, Typography, Switch, Tag, Tooltip } from 'antd'; -import MetricListItem from '../MetricListItem'; +import { + Table, + Typography, + Tag, + Tooltip, + Input, + Button, + Dropdown, + Modal as AntdModal, + Avatar +} from 'antd'; +import { TeamOutlined, LockOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { EllipsisVertical } from 'lucide-react'; import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface'; +import { useStore } from 'App/mstore'; +import { toast } from 'react-toastify'; +import { useHistory } from 'react-router'; +import { withSiteId } from 'App/routes'; +import { Icon } from 'UI'; +import cn from 'classnames'; +import { TYPE_ICONS, TYPE_NAMES } from 'App/constants/card'; import Widget from 'App/mstore/types/widget'; -import { LockOutlined, TeamOutlined } from "@ant-design/icons"; -import classNames from 'classnames'; const { Text } = Typography; -interface Metric { - metricId: number; - name: string; - owner: string; - lastModified: string; - visibility: string; -} - interface Props { list: Widget[]; siteId: string; selectedList: number[]; - toggleSelection?: (metricId: number | Array) => void; - toggleAll?: (e: any) => void; + toggleSelection?: (metricId: number | number[]) => void; disableSelection?: boolean; - allSelected?: boolean; - existingCardIds?: number[]; - showOwn?: boolean; - toggleOwn: () => void; inLibrary?: boolean; } -const ListView: React.FC = (props: Props) => { - const { - siteId, - list, - selectedList, - toggleSelection, - disableSelection = false, - inLibrary = false - } = props; +const ListView: React.FC = ({ + list, + siteId, + selectedList, + toggleSelection, + disableSelection = false, + inLibrary = false + }) => { const [sorter, setSorter] = useState<{ field: string; order: 'ascend' | 'descend' }>({ field: 'lastModified', order: 'descend' }); const [pagination, setPagination] = useState({ current: 1, pageSize: 10 }); - const totalMessage = ( - <> - Showing {pagination.pageSize * (pagination.current - 1) + 1} to {Math.min(pagination.pageSize * pagination.current, list.length)} of {list.length} cards - - ); + const [editingMetricId, setEditingMetricId] = useState(null); + const [newName, setNewName] = useState(''); + const { metricStore } = useStore(); + const history = useHistory(); const sortedData = useMemo(() => { return [...list].sort((a, b) => { @@ -59,33 +58,164 @@ const ListView: React.FC = (props: Props) => { ? new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime() : new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime(); } else if (sorter.field === 'name') { - return sorter.order === 'ascend' ? a.name?.localeCompare(b.name) : b.name?.localeCompare(a.name); + return sorter.order === 'ascend' + ? (a.name?.localeCompare(b.name) || 0) + : (b.name?.localeCompare(a.name) || 0); } else if (sorter.field === 'owner') { - return sorter.order === 'ascend' ? a.owner?.localeCompare(b.owner) : b.owner?.localeCompare(a.owner); + return sorter.order === 'ascend' + ? (a.owner?.localeCompare(b.owner) || 0) + : (b.owner?.localeCompare(a.owner) || 0); } return 0; }); }, [list, sorter]); const paginatedData = useMemo(() => { - const start = (pagination.current! - 1) * pagination.pageSize!; - const end = start + pagination.pageSize!; - return sortedData.slice(start, end).map(metric => ({ ...metric, key: metric.metricId})); + const start = ((pagination.current || 1) - 1) * (pagination.pageSize || 10); + return sortedData.slice(start, start + (pagination.pageSize || 10)); }, [sortedData, pagination]); + const totalMessage = ( + <> + Showing{' '} + + {(pagination.pageSize || 10) * ((pagination.current || 1) - 1) + 1} + {' '} + to{' '} + + {Math.min((pagination.pageSize || 10) * (pagination.current || 1), list.length)} + {' '} + of {list.length} cards + + ); + const handleTableChange = ( - pagination: TablePaginationConfig, - filters: Record, - sorter: SorterResult | SorterResult[] + pag: TablePaginationConfig, + _filters: Record, + sorterParam: SorterResult | SorterResult[] ) => { - const sortResult = sorter as SorterResult; + const sortRes = sorterParam as SorterResult; setSorter({ - field: sortResult.field as string, - order: sortResult.order as 'ascend' | 'descend' + field: sortRes.field as string, + order: sortRes.order as 'ascend' | 'descend' }); - setPagination(pagination); + setPagination(pag); }; + const parseDate = (dateString: string) => { + let date = new Date(dateString); + if (isNaN(date.getTime())) { + date = new Date(parseInt(dateString, 10)); + } + return date; + }; + + const formatDate = (date: Date) => { + const now = new Date(); + const diffTime = Math.abs(now.getTime() - date.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const formatTime = (d: Date) => { + let hours = d.getHours(); + const minutes = d.getMinutes().toString().padStart(2, '0'); + const ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12 || 12; + return `${hours}:${minutes} ${ampm}`; + }; + if (diffDays <= 1) return `Today at ${formatTime(date)}`; + if (diffDays === 2) return `Yesterday at ${formatTime(date)}`; + if (diffDays <= 3) return `${diffDays} days ago at ${formatTime(date)}`; + return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} at ${formatTime(date)}`; + }; + + const MetricTypeIcon: React.FC<{ type: string }> = ({ type }) => ( + {TYPE_NAMES[type]}}> + + } + size="default" + className="bg-tealx-lightest text-tealx mr-2 cursor-default avatar-card-list-item" + /> + + ); + + const onItemClick = (metric: Widget) => { + if (disableSelection) return; + if (toggleSelection) { + toggleSelection(metric.metricId); + } else { + const path = withSiteId(`/metrics/${metric.metricId}`, siteId); + history.push(path); + } + }; + + const onMenuClick = async (metric: Widget, { key }: { key: string }) => { + if (key === 'delete') { + AntdModal.confirm({ + title: 'Confirm', + content: 'Are you sure you want to permanently delete this card?', + okText: 'Yes, delete', + cancelText: 'No', + onOk: async () => { + await metricStore.delete(metric); + } + }); + } + if (key === 'rename') { + setEditingMetricId(metric.metricId); + setNewName(metric.name); + } + }; + + const onRename = async () => { + const metric = list.find((m) => m.metricId === editingMetricId); + if (!metric) return; + try { + metric.update({ name: newName }); + await metricStore.save(metric); + // await metricStore.fetchList(); + setEditingMetricId(null); + } catch (e) { + toast.error('Failed to rename card'); + } + }; + + const menuItems = [ + { key: 'rename', icon: , label: 'Rename' }, + { key: 'delete', icon: , label: 'Delete' } + ]; + + const renderTitle = (_text: string, metric: Widget) => ( +
onItemClick(metric)}> + +
+ {metric.name} +
+
+ ); + + const renderOwner = (_text: string, metric: Widget) =>
{metric.owner}
; + + const renderLastModified = (_text: string, metric: Widget) => { + const date = parseDate(metric.lastModified); + return formatDate(date); + }; + + const renderOptions = (_text: string, metric: Widget) => ( +
+ onMenuClick(metric, e) }} + trigger={['click']} + > +
+ ); + const columns = [ { title: 'Title', @@ -93,103 +223,89 @@ const ListView: React.FC = (props: Props) => { key: 'title', className: 'cap-first pl-4', sorter: true, - width: '25%', - render: (text: string, metric: Metric) => ( - - ) + width: inLibrary ? '31%' : '25%', + render: renderTitle }, { title: 'Owner', dataIndex: 'owner', key: 'owner', className: 'capitalize', - width: '25%', sorter: true, - render: (text: string, metric: Metric) => ( - - ) + width: inLibrary ? '31%' : '25%', + render: renderOwner }, { title: 'Last Modified', dataIndex: 'lastModified', key: 'lastModified', sorter: true, - width: '25%', - render: (text: string, metric: Metric) => ( - - ) - }, + width: inLibrary ? '31%' : '25%', + render: renderLastModified + } ]; if (!inLibrary) { columns.push({ - title: '', - key: 'options', - className: 'text-right', - width: '5%', - render: (text: string, metric: Metric) => ( - - ) - }) - } else { - columns.forEach(col => { - col.width = '31%'; - }) + title: '', + key: 'options', + className: 'text-right', + width: '5%', + render: renderOptions + }); } return ( - ({ - onClick: () => disableSelection ? null : toggleSelection?.(record.metricId) - }) : undefined} - rowSelection={ - !disableSelection - ? { - selectedRowKeys: selectedList, - onChange: (selectedRowKeys) => { - toggleSelection(selectedRowKeys); - }, - columnWidth: 16, - } - : undefined - } - pagination={{ - current: pagination.current, - pageSize: pagination.pageSize, - total: sortedData.length, - showSizeChanger: false, - className: 'px-4', - showLessItems: true, - showTotal: () => totalMessage, - size: 'small', - simple: 'true', - }} - /> + <> +
({ + onClick: () => { + if (!disableSelection) toggleSelection?.(record.metricId); + } + }) + : undefined + } + rowSelection={ + !disableSelection + ? { + selectedRowKeys: selectedList, + onChange: (keys) => toggleSelection && toggleSelection(keys), + columnWidth: 16 + } + : undefined + } + pagination={{ + current: pagination.current, + pageSize: pagination.pageSize, + total: sortedData.length, + showSizeChanger: false, + className: 'px-4', + showLessItems: true, + showTotal: () => totalMessage, + size: 'small', + simple: true + }} + /> + setEditingMetricId(null)} + > + setNewName(e.target.value)} + /> + + ); };