ui: fixes for card library
This commit is contained in:
parent
79f3cb7d98
commit
7f58f73c98
6 changed files with 152 additions and 122 deletions
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue