openreplay/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx
2025-03-12 14:41:33 +01:00

316 lines
8.8 KiB
TypeScript

import React, { useState } from 'react';
import {
Table,
Typography,
Tooltip,
Input,
Button,
Dropdown,
Modal as AntdModal,
Avatar, TableColumnType, Spin
} from 'antd';
import {
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 { useTranslation } from 'react-i18next';
import ORLoader from 'Shared/ORLoader';
const { Text } = Typography;
interface Props {
list: Widget[];
siteId: string;
selectedList: number[];
toggleSelection?: (metricId: number | number[]) => void;
disableSelection?: boolean;
inLibrary?: boolean;
loading?: boolean;
}
const ListView: React.FC<Props> = ({
list,
siteId,
selectedList,
toggleSelection,
disableSelection = false,
inLibrary = false,
loading = false
}) => {
const { t } = useTranslation();
const [editingMetricId, setEditingMetricId] = useState<number | null>(null);
const [newName, setNewName] = useState('');
const { metricStore } = useStore();
const history = useHistory();
const totalMessage = (
<>
{t('Showing')}{' '}
<Text strong>
{(metricStore.pageSize || 10) * ((metricStore.page || 1) - 1) + 1}
</Text>{' '}
{t('to')}{' '}
<Text strong>
{Math.min(
(metricStore.pageSize || 10) * (metricStore.page || 1),
list.length
)}
</Text>{' '}
{t('of')}&nbsp;<Text strong>{list.length}</Text>&nbsp;{t('cards')}
</>
);
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 `${t('Today at')} ${formatTime(date)}`;
if (diffDays === 2) return `${t('Yesterday at')} ${formatTime(date)}`;
if (diffDays <= 3)
return `${diffDays} ${t('days ago at')} ${formatTime(date)}`;
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${t('at')} ${formatTime(date)}`;
};
const MetricTypeIcon: React.FC<{ type: string }> = ({ type }) => (
<Tooltip title={<div className="capitalize">{TYPE_NAMES(t)[type]}</div>}>
<Avatar
src={
<Icon
name={TYPE_ICONS[type]}
size="16"
color="tealx"
strokeColor="tealx"
/>
}
size="default"
className="bg-tealx-lightest text-tealx mr-2 cursor-default avatar-card-list-item"
/>
</Tooltip>
);
const onItemClick = (metric: Widget) => {
if (disableSelection) {
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
history.push(path);
} else {
toggleSelection?.(metric.metricId);
}
};
const onMenuClick = async (metric: Widget, { key }: { key: string }) => {
if (key === 'delete') {
AntdModal.confirm({
title: t('Confirm'),
content: t('Are you sure you want to permanently delete this card?'),
okText: t('Yes, delete'),
cancelText: t('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(t('Failed to rename card'));
}
};
const menuItems = [
{ key: 'rename', icon: <EditOutlined />, label: t('Rename') },
{ key: 'delete', icon: <DeleteOutlined />, label: t('Delete') }
];
const renderTitle = (_text: string, metric: Widget) => (
<div
className="flex items-center cursor-pointer"
onClick={() => onItemClick(metric)}
>
<MetricTypeIcon type={metric.metricType} />
<div className={cn('capitalize-first block', !inLibrary ? 'link' : '')}>
{metric.name}
</div>
</div>
);
const renderOwner = (_text: string, metric: Widget) => (
<div>{metric.owner}</div>
);
const renderLastModified = (_text: string, metric: Widget) => {
const date = parseDate(metric.lastModified);
return formatDate(date);
};
const renderOptions = (_text: string, metric: Widget) => (
<div className="flex justify-end pr-4">
<Dropdown
menu={{ items: menuItems, onClick: (e) => onMenuClick(metric, e) }}
trigger={['click']}
>
<Button
icon={<EllipsisVertical size={16} />}
className="btn-cards-list-item-more-options"
type="text"
/>
</Dropdown>
</div>
);
const columns: TableColumnType<any>[] = [
{
title: t('Title'),
dataIndex: 'name',
key: 'title',
className: 'cap-first pl-4',
sorter: true,
sortOrder: metricStore.sort.field === 'name' ? metricStore.sort.order : undefined,
width: inLibrary ? '31%' : '25%',
render: renderTitle
},
{
title: t('Owner'),
dataIndex: 'owner_email',
key: 'owner',
className: 'capitalize',
sorter: true,
sortOrder: metricStore.sort.field === 'owner_email' ? metricStore.sort.order : undefined,
width: inLibrary ? '31%' : '25%',
render: renderOwner
},
{
title: t('Last Modified'),
dataIndex: 'edited_at',
key: 'lastModified',
sorter: true,
sortOrder: metricStore.sort.field === 'edited_at' ? metricStore.sort.order : undefined,
width: inLibrary ? '31%' : '25%',
render: renderLastModified
}
];
if (!inLibrary) {
columns.push({
title: '',
key: 'options',
className: 'text-right',
width: '5%',
render: renderOptions
});
}
const handleTableChange = (
pag: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorterParam: SorterResult<Widget> | SorterResult<Widget>[]
) => {
const sorter = Array.isArray(sorterParam) ? sorterParam[0] : sorterParam;
let order = sorter.order;
if (metricStore.sort.field === sorter.field) {
order = metricStore.sort.order === 'ascend' ? 'descend' : 'ascend';
}
console.log('sorter', { field: sorter.field, order });
metricStore.updateKey('sort', { field: sorter.field, order });
metricStore.updateKey('page', pag.current || 1);
};
return (
<>
<Table
columns={columns}
dataSource={list}
rowKey="metricId"
showSorterTooltip={false}
onChange={handleTableChange}
sortDirections={['ascend', 'descend']}
loading={{
spinning: loading,
delay: 0,
indicator: <Spin indicator={<ORLoader />} />,
}}
onRow={
inLibrary
? (record) => ({
onClick: () => {
if (!disableSelection) toggleSelection?.(record?.metricId);
}
})
: undefined
}
rowSelection={
!disableSelection
? {
selectedRowKeys: selectedList,
onChange: (keys) => toggleSelection && toggleSelection(keys),
columnWidth: 16
}
: undefined
}
pagination={{
current: metricStore.page,
pageSize: metricStore.pageSize,
total: metricStore.total,
showSizeChanger: false,
className: 'px-4',
showLessItems: true,
showTotal: () => totalMessage,
size: 'small',
simple: true
}}
/>
<AntdModal
title={t('Rename Card')}
open={editingMetricId !== null}
okText={t('Save')}
cancelText={t('Cancel')}
onOk={onRename}
onCancel={() => setEditingMetricId(null)}
>
<Input
placeholder={t('Enter new card title')}
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
</AntdModal>
</>
);
};
export default ListView;