ui: fixes for card library

This commit is contained in:
nick-delirium 2025-01-15 17:47:18 +01:00
parent 79f3cb7d98
commit 7f58f73c98
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
6 changed files with 152 additions and 122 deletions

View file

@ -126,16 +126,11 @@ export function customTooltipFormatter(uuid: string) {
if (partnerValue !== undefined) {
const partnerColor =
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
str += `<div class="flex gap-2 items-center mt-2">
<div style="
border-radius: 99px;
background: ${partnerColor};
width: 1rem;
height: 1rem;">
</div>
<div class="font-medium">${partnerName}</div>
</div>
str += `
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
<div class="text-neutral-600 text-sm">
${isPrevious ? 'Current' : 'Previous'} Total:
</div>
<div class="flex items-center gap-1">
<div class="font-medium">${partnerValue ?? '—'}</div>
${buildCompareTag(partnerValue, params.value)}

View file

@ -1,7 +1,23 @@
import React, { useEffect, useState } from 'react';
import { Icon, Modal } from 'UI';
import { Tooltip, Input, Button, Dropdown, Menu, Tag, Modal as AntdModal, Form, Avatar } from 'antd';
import { TeamOutlined, LockOutlined, EditOutlined, DeleteOutlined, MoreOutlined } from '@ant-design/icons';
import {
Tooltip,
Input,
Button,
Dropdown,
Menu,
Tag,
Modal as AntdModal,
Form,
Avatar,
} from 'antd';
import {
TeamOutlined,
LockOutlined,
EditOutlined,
DeleteOutlined,
MoreOutlined,
} from '@ant-design/icons';
import { RouteComponentProps } from 'react-router-dom';
import { withSiteId } from 'App/routes';
import { TYPE_ICONS, TYPE_NAMES } from 'App/constants/card';
@ -9,33 +25,45 @@ import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { toast } from 'react-toastify';
import { useHistory } from 'react-router';
import { EllipsisVertical } from "lucide-react";
import { EllipsisVertical } from 'lucide-react';
import cn from 'classnames'
interface Props extends RouteComponentProps {
metric: any;
siteId: string;
selected?: boolean;
toggleSelection?: any;
disableSelection?: boolean;
renderColumn: string;
inLibrary?: boolean;
}
function MetricTypeIcon({ type }: any) {
return (
<Tooltip title={<div className="capitalize">{TYPE_NAMES[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" />
<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 MetricListItem: React.FC<Props> = ({
metric,
siteId,
toggleSelection = () => {
},
disableSelection = false,
renderColumn
}) => {
metric,
siteId,
toggleSelection = () => {},
disableSelection = false,
renderColumn,
inLibrary,
}) => {
const history = useHistory();
const { metricStore } = useStore();
const [isEdit, setIsEdit] = useState(false);
@ -62,7 +90,7 @@ const MetricListItem: React.FC<Props> = ({
cancelText: 'No',
onOk: async () => {
await metricStore.delete(metric);
}
},
});
}
if (key === 'rename') {
@ -127,29 +155,34 @@ const MetricListItem: React.FC<Props> = ({
} else if (diffDays <= 3) {
return `${diffDays} days ago at ${formatTime(date)}`;
} else {
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} at ${formatTime(date)}`;
return `${date.getDate()}/${
date.getMonth() + 1
}/${date.getFullYear()} at ${formatTime(date)}`;
}
};
const menuItems = [
{
key: "rename",
key: 'rename',
icon: <EditOutlined />,
label: "Rename"
label: 'Rename',
},
{
key: "delete",
key: 'delete',
icon: <DeleteOutlined />,
label: "Delete"
}
]
label: 'Delete',
},
];
switch (renderColumn) {
case 'title':
return (
<>
<div className="flex items-center cursor-pointer" onClick={onItemClick}>
<div
className="flex items-center cursor-pointer"
onClick={inLibrary ? undefined : onItemClick}
>
<MetricTypeIcon type={metric.metricType} />
<div className="capitalize-first link block">{metric.name}</div>
<div className={cn('capitalize-first block', inLibrary ? '' : 'link')}>{metric.name}</div>
</div>
{renderModal()}
</>
@ -160,7 +193,11 @@ const MetricListItem: React.FC<Props> = ({
return (
<div className="flex items-center">
<Tag className="rounded-lg" bordered={false}>
{metric.isPublic ? <TeamOutlined className="mr-2" /> : <LockOutlined className="mr-2" />}
{metric.isPublic ? (
<TeamOutlined className="mr-2" />
) : (
<LockOutlined className="mr-2" />
)}
{metric.isPublic ? 'Team' : 'Private'}
</Tag>
</div>
@ -171,13 +208,18 @@ const MetricListItem: React.FC<Props> = ({
case 'options':
return (
<>
<div className='flex justify-end pr-4'>
<Dropdown
menu={{ items: menuItems, onClick: onMenuClick }}
trigger={['click']}
>
<Button id={'ignore-prop'} icon={<EllipsisVertical size={16} />} className='btn-cards-list-item-more-options' type='text'/>
</Dropdown>
<div className="flex justify-end pr-4">
<Dropdown
menu={{ items: menuItems, onClick: onMenuClick }}
trigger={['click']}
>
<Button
id={'ignore-prop'}
icon={<EllipsisVertical size={16} />}
className="btn-cards-list-item-more-options"
type="text"
/>
</Dropdown>
</div>
{renderModal()}
</>

View file

@ -5,6 +5,7 @@ import { Icon } from 'UI';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import FooterContent from './FooterContent';
import { Input } from 'antd'
interface Props {
dashboardId?: number;
@ -46,7 +47,7 @@ function MetricsLibraryModal(props: Props) {
</Modal.Header>
<Modal.Content className="p-4 pb-20">
<div className="border">
<MetricsList siteId={siteId} onSelectionChange={onSelectionChange} />
<MetricsList siteId={siteId} onSelectionChange={onSelectionChange} inLibrary />
</div>
</Modal.Content>
<Modal.Footer>
@ -61,12 +62,11 @@ export default observer(MetricsLibraryModal);
function MetricSearch({ onChange }: any) {
return (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
<Input.Search
name="dashboardsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title or owner"
onChange={onChange}
className={'rounded-lg'}
/>
</div>
);

View file

@ -20,13 +20,14 @@ interface Props {
list: Widget[];
siteId: string;
selectedList: number[];
toggleSelection?: (metricId: number) => void;
toggleSelection?: (metricId: number | Array<number>) => void;
toggleAll?: (e: any) => void;
disableSelection?: boolean;
allSelected?: boolean;
existingCardIds?: number[];
showOwn?: boolean;
toggleOwn: () => void;
inLibrary?: boolean;
}
const ListView: React.FC<Props> = (props: Props) => {
@ -36,10 +37,7 @@ const ListView: React.FC<Props> = (props: Props) => {
selectedList,
toggleSelection,
disableSelection = false,
allSelected = false,
toggleAll,
showOwn,
toggleOwn,
inLibrary = false
} = props;
const [sorter, setSorter] = useState<{ field: string; order: 'ascend' | 'descend' }>({
field: 'lastModified',
@ -72,7 +70,7 @@ const ListView: React.FC<Props> = (props: Props) => {
const paginatedData = useMemo(() => {
const start = (pagination.current! - 1) * pagination.pageSize!;
const end = start + pagination.pageSize!;
return sortedData.slice(start, end);
return sortedData.slice(start, end).map(metric => ({ ...metric, key: metric.metricId}));
}, [sortedData, pagination]);
const handleTableChange = (
@ -90,19 +88,7 @@ const ListView: React.FC<Props> = (props: Props) => {
const columns = [
{
title: (
<div className="flex items-center">
{!disableSelection && (
<Checkbox
name="slack"
className="mr-4"
checked={allSelected}
onClick={toggleAll}
/>
)}
<span>Title</span>
</div>
),
title: 'Title',
dataIndex: 'name',
key: 'title',
className: 'cap-first pl-4',
@ -113,12 +99,8 @@ const ListView: React.FC<Props> = (props: Props) => {
key={metric.metricId}
metric={metric}
siteId={siteId}
disableSelection={disableSelection}
selected={selectedList.includes(metric.metricId)}
toggleSelection={(e: any) => {
e.stopPropagation();
toggleSelection && toggleSelection(metric.metricId);
}}
inLibrary={inLibrary}
disableSelection={!inLibrary}
renderColumn="title"
/>
)
@ -128,7 +110,7 @@ const ListView: React.FC<Props> = (props: Props) => {
dataIndex: 'owner',
key: 'owner',
className: 'capitalize',
width: '16.67%',
width: '25%',
sorter: true,
render: (text: string, metric: Metric) => (
<MetricListItem
@ -144,7 +126,7 @@ const ListView: React.FC<Props> = (props: Props) => {
dataIndex: 'lastModified',
key: 'lastModified',
sorter: true,
width: '16.67%',
width: '25%',
render: (text: string, metric: Metric) => (
<MetricListItem
key={metric.metricId}
@ -154,21 +136,27 @@ const ListView: React.FC<Props> = (props: Props) => {
/>
)
},
{
title: '',
key: 'options',
className: 'text-right',
width: '5%',
render: (text: string, metric: Metric) => (
<MetricListItem
key={metric.metricId}
metric={metric}
siteId={siteId}
renderColumn="options"
/>
)
}
];
if (!inLibrary) {
columns.push({
title: '',
key: 'options',
className: 'text-right',
width: '5%',
render: (text: string, metric: Metric) => (
<MetricListItem
key={metric.metricId}
metric={metric}
siteId={siteId}
renderColumn="options"
/>
)
})
} else {
columns.forEach(col => {
col.width = '31%';
})
}
return (
<Table
@ -176,15 +164,17 @@ const ListView: React.FC<Props> = (props: Props) => {
dataSource={paginatedData}
rowKey="metricId"
onChange={handleTableChange}
onRow={inLibrary ? (record) => ({
onClick: () => disableSelection ? null : toggleSelection?.(record.metricId)
}) : undefined}
rowSelection={
!disableSelection
? {
selectedRowKeys: selectedList.map((id: number) => id.toString()),
selectedRowKeys: selectedList,
onChange: (selectedRowKeys) => {
selectedRowKeys.forEach((key: any) => {
toggleSelection && toggleSelection(parseInt(key));
});
}
toggleSelection(selectedRowKeys);
},
columnWidth: 16,
}
: undefined
}

View file

@ -10,13 +10,15 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
function MetricsList({
siteId,
onSelectionChange,
inLibrary,
}: {
siteId: string;
onSelectionChange?: (selected: any[]) => void;
inLibrary?: boolean;
}) {
const { metricStore, dashboardStore } = useStore();
const metricsSearch = metricStore.filter.query;
const listView = metricStore.listView;
const listView = inLibrary ? true : metricStore.listView;
const [selectedMetrics, setSelectedMetrics] = useState<any>([]);
const dashboard = dashboardStore.selectedDashboard;
@ -47,10 +49,14 @@ function MetricsList({
}, [selectedMetrics]);
const toggleMetricSelection = (id: any) => {
if (Array.isArray(id)) {
setSelectedMetrics(id);
return
}
if (selectedMetrics.includes(id)) {
setSelectedMetrics(selectedMetrics.filter((i: number) => i !== id));
setSelectedMetrics((prev) => prev.filter((i: number) => i !== id));
} else {
setSelectedMetrics([...selectedMetrics, id]);
setSelectedMetrics((prev) => [...prev, id]);
}
};
@ -89,6 +95,7 @@ function MetricsList({
disableSelection={!onSelectionChange}
siteId={siteId}
list={cards}
inLibrary={inLibrary}
selectedList={selectedMetrics}
existingCardIds={existingCardIds}
toggleSelection={toggleMetricSelection}

View file

@ -4,7 +4,7 @@ import type { TableProps } from 'antd';
import { Eye, EyeOff } from 'lucide-react';
import cn from 'classnames';
import React, { useState } from 'react';
import { TableExporter } from '../../../Funnels/FunnelWidget/FunnelTable';
import { TableExporter } from 'Components/Funnels/FunnelWidget/FunnelTable';
const initTableProps = [
{
@ -62,15 +62,7 @@ function WidgetDatatable(props: Props) {
const [showTable, setShowTable] = useState(props.defaultOpen);
const [tableData, setTableData] = useState([]);
const columnNames = new Set();
/**
* basically we have an array of
* { time: some_date, series1: 1, series2: 2, series3: 3, timestamp: 123456 }
* which we turn into a table where each series of filters = row;
* and each unique time = column
* + average for each row
* [ { seriesName: 'series1', mon: 1, tue: 2, wed: 3, average: 2 }, ... ]
* */
const columnNames = [];
const series = !data.chart[0]
? []
: data.namesMap;
@ -78,15 +70,16 @@ function WidgetDatatable(props: Props) {
React.useEffect(() => {
if (!data.chart) return;
setTableProps(initTableProps);
columnNames.clear();
data.chart.forEach((p: any) => {
columnNames.add(p.time);
}); // for example: mon, tue, wed, thu, fri, sat, sun
const avg: any = {}; // { seriesName: {itemsCount: 0, total: 0} }
const items: Record<string, any>[] = []; // as many items (rows) as we have series in filter
columnNames.length = data.chart.length;
// for example: mon, tue, wed, thu, fri, sat, sun
data.chart.forEach((p: any, i) => {
columnNames[i] = p.time;
});
// as many items (rows) as we have series in filter
const items: Record<string, any>[] = [];
series.forEach((s, i) => {
items.push({ seriesName: s, average: 0, key: s });
avg[s] = { itemsCount: 0, total: 0 };
});
const tableCols: {
title: React.ReactNode;
@ -94,27 +87,30 @@ function WidgetDatatable(props: Props) {
key: string;
sorter: any;
}[] = [];
const uniqueColArr = Array.from(columnNames);
uniqueColArr.forEach((name: string, i) => {
columnNames.forEach((name: string, i) => {
tableCols.push({
title: <span className={'font-medium'}>{name}</span>,
dataIndex: name,
key: name,
sorter: (a, b) => a[name] - b[name],
dataIndex: name+'_'+i,
key: name+'_'+i,
sorter: (a, b) => a[name+'_'+i] - b[name+'_'+i],
});
const values = data.chart.filter((p) => p.time === name);
const values = data.chart[i];
series.forEach((s) => {
avg[s].itemsCount += 1;
avg[s].total += values.reduce((acc, curr) => acc + curr[s], 0);
const ind = items.findIndex((item) => item.seriesName === s);
if (ind === -1) return;
items[ind][name] = values.reduce((acc, curr) => acc + curr[s], 0);
items[ind][name+'_'+i] = values[s];
});
});
Object.keys(avg).forEach((key) => {
const ind = items.findIndex((item) => item.seriesName === key);
if (ind === -1) return;
items[ind].average = (avg[key].total / avg[key].itemsCount).toFixed(2);
// calculating averages for each row
items.forEach((item) => {
const itemsLen = columnNames.length;
const keys = Object.keys(item).filter(k => !['seriesName', 'key', 'average'].includes(k));
let sum = 0;
const values = keys.map(k => item[k]);
values.forEach((v) => {
sum += v;
});
item.average = (sum / itemsLen).toFixed(1);
});
setTableProps((prev) => [...prev, ...tableCols]);