feat(metrics): implement server-side pagination and sorting

Refactors metrics list view to use server-side pagination and sorting
instead of client-side implementation. This improves performance for
large datasets by reducing client workload and network payload size.

Key changes:
- Add pagination API endpoint in MetricService
- Update MetricStore to handle server pagination
- Refactor ListView component to use server-side sorting
- Remove client-side sorting and pagination logic
This commit is contained in:
Shekar Siri 2025-03-10 14:54:27 +01:00
parent 4b09213448
commit 687ab05f22
5 changed files with 193 additions and 256 deletions

View file

@ -41,9 +41,9 @@ function MetricViewHeader() {
// Show header if there are cards or if a filter is active // Show header if there are cards or if a filter is active
const showHeader = cardsLength > 0 || isFilterActive; const showHeader = cardsLength > 0 || isFilterActive;
useEffect(() => { // useEffect(() => {
metricStore.updateKey('sort', { by: 'desc' }); // metricStore.updateKey('sort', { by: 'desc' });
}, [metricStore]); // }, [metricStore]);
const handleMenuClick = ({ key }: { key: string }) => { const handleMenuClick = ({ key }: { key: string }) => {
metricStore.updateKey('filter', { ...filter, type: key }); metricStore.updateKey('filter', { ...filter, type: key });

View file

@ -8,13 +8,13 @@ import {
Button, Button,
Dropdown, Dropdown,
Modal as AntdModal, Modal as AntdModal,
Avatar, Avatar, TableColumnType
} from 'antd'; } from 'antd';
import { import {
TeamOutlined, TeamOutlined,
LockOutlined, LockOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { EllipsisVertical } from 'lucide-react'; import { EllipsisVertical } from 'lucide-react';
import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface'; import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface';
@ -37,90 +37,41 @@ interface Props {
toggleSelection?: (metricId: number | number[]) => void; toggleSelection?: (metricId: number | number[]) => void;
disableSelection?: boolean; disableSelection?: boolean;
inLibrary?: boolean; inLibrary?: boolean;
loading?: boolean;
} }
const ListView: React.FC<Props> = ({ const ListView: React.FC<Props> = ({
list, list,
siteId, siteId,
selectedList, selectedList,
toggleSelection, toggleSelection,
disableSelection = false, disableSelection = false,
inLibrary = false inLibrary = false,
}) => { loading = false
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [sorter, setSorter] = useState<{ field: string; order: 'ascend' | 'descend' }>({
field: 'lastModified',
order: 'descend',
});
const [pagination, setPagination] = useState<TablePaginationConfig>({
current: 1,
pageSize: 10,
});
const [editingMetricId, setEditingMetricId] = useState<number | null>(null); const [editingMetricId, setEditingMetricId] = useState<number | null>(null);
const [newName, setNewName] = useState(''); const [newName, setNewName] = useState('');
const { metricStore } = useStore(); const { metricStore } = useStore();
const history = useHistory(); const history = useHistory();
const sortedData = useMemo(
() =>
[...list].sort((a, b) => {
if (sorter.field === 'lastModified') {
return sorter.order === 'ascend'
? new Date(a.lastModified).getTime() -
new Date(b.lastModified).getTime()
: new Date(b.lastModified).getTime() -
new Date(a.lastModified).getTime();
}
if (sorter.field === 'name') {
return sorter.order === 'ascend'
? a.name?.localeCompare(b.name) || 0
: b.name?.localeCompare(a.name) || 0;
}
if (sorter.field === '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) - 1) * (pagination.pageSize || 10);
return sortedData.slice(start, start + (pagination.pageSize || 10));
}, [sortedData, pagination]);
const totalMessage = ( const totalMessage = (
<> <>
{t('Showing')}{' '} {t('Showing')}{' '}
<Text strong> <Text strong>
{(pagination.pageSize || 10) * ((pagination.current || 1) - 1) + 1} {(metricStore.pageSize || 10) * ((metricStore.page || 1) - 1) + 1}
</Text>{' '} </Text>{' '}
{t('to')}{' '} {t('to')}{' '}
<Text strong> <Text strong>
{Math.min( {Math.min(
(pagination.pageSize || 10) * (pagination.current || 1), (metricStore.pageSize || 10) * (metricStore.page || 1),
list.length, list.length
)} )}
</Text>{' '} </Text>{' '}
{t('of')}&nbsp;<Text strong>{list.length}</Text>&nbsp;{t('cards')} {t('of')}&nbsp;<Text strong>{list.length}</Text>&nbsp;{t('cards')}
</> </>
); );
const handleTableChange = (
pag: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorterParam: SorterResult<Widget> | SorterResult<Widget>[],
) => {
const sortRes = sorterParam as SorterResult<Widget>;
setSorter({
field: sortRes.field as string,
order: sortRes.order as 'ascend' | 'descend',
});
setPagination(pag);
};
const parseDate = (dateString: string) => { const parseDate = (dateString: string) => {
let date = new Date(dateString); let date = new Date(dateString);
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
@ -182,7 +133,7 @@ const ListView: React.FC<Props> = ({
cancelText: t('No'), cancelText: t('No'),
onOk: async () => { onOk: async () => {
await metricStore.delete(metric); await metricStore.delete(metric);
}, }
}); });
} }
if (key === 'rename') { if (key === 'rename') {
@ -206,7 +157,7 @@ const ListView: React.FC<Props> = ({
const menuItems = [ const menuItems = [
{ key: 'rename', icon: <EditOutlined />, label: t('Rename') }, { key: 'rename', icon: <EditOutlined />, label: t('Rename') },
{ key: 'delete', icon: <DeleteOutlined />, label: t('Delete') }, { key: 'delete', icon: <DeleteOutlined />, label: t('Delete') }
]; ];
const renderTitle = (_text: string, metric: Widget) => ( const renderTitle = (_text: string, metric: Widget) => (
@ -245,80 +196,109 @@ const ListView: React.FC<Props> = ({
</div> </div>
); );
const columns = [ const columns: TableColumnType<any>[] = [
{ {
title: t('Title'), title: t('Title'),
dataIndex: 'name', dataIndex: 'name',
key: 'title', key: 'title',
className: 'cap-first pl-4', className: 'cap-first pl-4',
sorter: true, sorter: true,
sortOrder: metricStore.sort.field === 'name' ? metricStore.sort.order : undefined,
width: inLibrary ? '31%' : '25%', width: inLibrary ? '31%' : '25%',
render: renderTitle, render: renderTitle
}, },
{ {
title: t('Owner'), title: t('Owner'),
dataIndex: 'owner', dataIndex: 'owner_email',
key: 'owner', key: 'owner',
className: 'capitalize', className: 'capitalize',
sorter: true, sorter: true,
sortOrder: metricStore.sort.field === 'owner_email' ? metricStore.sort.order : undefined,
width: inLibrary ? '31%' : '25%', width: inLibrary ? '31%' : '25%',
render: renderOwner, render: renderOwner
}, },
{ {
title: t('Last Modified'), title: t('Last Modified'),
dataIndex: 'lastModified', dataIndex: 'edited_at',
key: 'lastModified', key: 'lastModified',
sorter: true, sorter: true,
sortOrder: metricStore.sort.field === 'edited_at' ? metricStore.sort.order : undefined,
width: inLibrary ? '31%' : '25%', width: inLibrary ? '31%' : '25%',
render: renderLastModified, render: renderLastModified
}, }
]; ];
if (!inLibrary) { if (!inLibrary) {
columns.push({ columns.push({
title: '', title: '',
key: 'options', key: 'options',
className: 'text-right', className: 'text-right',
width: '5%', width: '5%',
render: renderOptions, render: renderOptions
}); });
} }
// if (metricStore.sort.field) {
// columns.forEach((col) => {
// col.sortOrder = col.key === metricStore.sort.field ? metricStore.sort.order : false;
// });
// }
console.log('store', metricStore.sort);
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 ( return (
<> <>
<Table <Table
loading={loading}
columns={columns} columns={columns}
dataSource={paginatedData} dataSource={list}
rowKey="metricId" rowKey="metricId"
showSorterTooltip={false} showSorterTooltip={false}
onChange={handleTableChange} onChange={handleTableChange}
sortDirections={['ascend', 'descend']}
onRow={ onRow={
inLibrary inLibrary
? (record) => ({ ? (record) => ({
onClick: () => { onClick: () => {
if (!disableSelection) toggleSelection?.(record?.metricId); if (!disableSelection) toggleSelection?.(record?.metricId);
}, }
}) })
: undefined : undefined
} }
rowSelection={ rowSelection={
!disableSelection !disableSelection
? { ? {
selectedRowKeys: selectedList, selectedRowKeys: selectedList,
onChange: (keys) => toggleSelection && toggleSelection(keys), onChange: (keys) => toggleSelection && toggleSelection(keys),
columnWidth: 16, columnWidth: 16
} }
: undefined : undefined
} }
pagination={{ pagination={{
current: pagination.current, current: metricStore.page,
pageSize: pagination.pageSize, pageSize: metricStore.pageSize,
total: sortedData.length, total: metricStore.total,
showSizeChanger: false, showSizeChanger: false,
className: 'px-4', className: 'px-4',
showLessItems: true, showLessItems: true,
showTotal: () => totalMessage, showTotal: () => totalMessage,
size: 'small', size: 'small',
simple: true, simple: true
}} }}
/> />
<AntdModal <AntdModal

View file

@ -6,16 +6,15 @@ import { sliceListPerPage } from 'App/utils';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { Popover, Button } from 'antd'; import { Popover, Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import GridView from './GridView';
import ListView from './ListView'; import ListView from './ListView';
import AddCardSection from '../AddCardSection/AddCardSection'; import AddCardSection from '../AddCardSection/AddCardSection';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
function MetricsList({ function MetricsList({
siteId, siteId,
onSelectionChange, onSelectionChange,
inLibrary, inLibrary
}: { }: {
siteId: string; siteId: string;
onSelectionChange?: (selected: any[]) => void; onSelectionChange?: (selected: any[]) => void;
inLibrary?: boolean; inLibrary?: boolean;
@ -23,28 +22,27 @@ function MetricsList({
const { t } = useTranslation(); const { t } = useTranslation();
const { metricStore, dashboardStore } = useStore(); const { metricStore, dashboardStore } = useStore();
const metricsSearch = metricStore.filter.query; const metricsSearch = metricStore.filter.query;
const listView = inLibrary ? true : metricStore.listView;
const [selectedMetrics, setSelectedMetrics] = useState<any>([]); const [selectedMetrics, setSelectedMetrics] = useState<any>([]);
const dashboard = dashboardStore.selectedDashboard; const dashboard = dashboardStore.selectedDashboard;
const existingCardIds = useMemo( const existingCardIds = useMemo(
() => dashboard?.widgets?.map((i) => parseInt(i.metricId)), () => dashboard?.widgets?.map((i) => parseInt(i.metricId)),
[dashboard], [dashboard]
); );
const cards = useMemo( const cards = useMemo(
() => () =>
onSelectionChange onSelectionChange
? metricStore.filteredCards.filter( ? metricStore.filteredCards.filter(
(i) => !existingCardIds?.includes(parseInt(i.metricId)), (i) => !existingCardIds?.includes(parseInt(i.metricId))
) )
: metricStore.filteredCards, : metricStore.filteredCards,
[metricStore.filteredCards, existingCardIds, onSelectionChange], [metricStore.filteredCards, existingCardIds, onSelectionChange]
); );
const loading = metricStore.isLoading; const loading = metricStore.isLoading;
useEffect(() => { useEffect(() => {
void metricStore.fetchList(); void metricStore.fetchList();
}, [metricStore]); }, [metricStore.page, metricStore.filter, metricStore.sort]);
useEffect(() => { useEffect(() => {
if (!onSelectionChange) return; if (!onSelectionChange) return;
@ -69,14 +67,8 @@ function MetricsList({
metricStore.updateKey('sessionsPage', 1); metricStore.updateKey('sessionsPage', 1);
}, [metricStore]); }, [metricStore]);
const showOwn = metricStore.filter.showMine;
const toggleOwn = () => {
metricStore.updateKey('showMine', !showOwn);
};
const isFiltered = const isFiltered = metricStore.filter.query !== '' || metricStore.filter.type !== 'all';
metricsSearch !== '' ||
(metricStore.filter.type && metricStore.filter.type !== 'all');
const searchImageDimensions = { width: 60, height: 'auto' }; const searchImageDimensions = { width: 60, height: 'auto' };
const defaultImageDimensions = { width: 600, height: 'auto' }; const defaultImageDimensions = { width: 600, height: 'auto' };
@ -86,101 +78,65 @@ function MetricsList({
: defaultImageDimensions; : defaultImageDimensions;
return ( return (
<Loader loading={loading}> <NoContent
<NoContent show={!loading && length === 0}
show={length === 0} title={
title={ <div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center justify-center"> <AnimatedSVG name={emptyImage} size={imageDimensions.width} />
<AnimatedSVG name={emptyImage} size={imageDimensions.width} /> <div className="text-center mt-3 text-lg font-medium">
<div className="text-center mt-3 text-lg font-medium"> {isFiltered
{isFiltered ? t('No matching results')
? t('No matching results') : t('Unlock insights with data cards')}
: t('Unlock insights with data cards')}
</div>
</div> </div>
} </div>
subtext={ }
isFiltered ? ( subtext={
'' isFiltered ? (
) : ( ''
<div className="flex flex-col items-center">
<div>
{t('Create and customize cards to analyze trends and user behavior effectively.')}
</div>
<Popover
arrow={false}
overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }}
content={<AddCardSection fit inCards />}
trigger="click"
>
<Button
type="primary"
icon={<PlusOutlined />}
className="btn-create-card mt-3"
>
{t('Create Card')}
</Button>
</Popover>
</div>
)
}
>
{listView ? (
<ListView
disableSelection={!onSelectionChange}
siteId={siteId}
list={cards}
inLibrary={inLibrary}
selectedList={selectedMetrics}
existingCardIds={existingCardIds}
toggleSelection={toggleMetricSelection}
allSelected={cards.length === selectedMetrics.length}
showOwn={showOwn}
toggleOwn={toggleOwn}
toggleAll={({ target: { checked } }) =>
setSelectedMetrics(
checked
? cards
.map((i: any) => i.metricId)
.slice(0, 30 - (existingCardIds?.length || 0))
: [],
)
}
/>
) : ( ) : (
<> <div className="flex flex-col items-center">
<GridView <div>
siteId={siteId} {t('Create and customize cards to analyze trends and user behavior effectively.')}
list={sliceListPerPage(
cards,
metricStore.page - 1,
metricStore.pageSize,
)}
selectedList={selectedMetrics}
toggleSelection={toggleMetricSelection}
/>
<div className="w-full flex items-center justify-between py-4 px-6 border-t">
<div>
{t('Showing')}{' '}
<span className="font-medium">
{Math.min(cards.length, metricStore.pageSize)}
</span>{' '}
{t('out of')}&nbsp;
<span className="font-medium">{cards.length}</span>&nbsp;
{t('cards')}
</div>
<Pagination
page={metricStore.page}
total={length}
onPageChange={(page) => metricStore.updateKey('page', page)}
limit={metricStore.pageSize}
debounceRequest={100}
/>
</div> </div>
</> <Popover
)} arrow={false}
</NoContent> overlayInnerStyle={{ padding: 0, borderRadius: '0.75rem' }}
</Loader> content={<AddCardSection fit inCards />}
trigger="click"
>
<Button
type="primary"
icon={<PlusOutlined />}
className="btn-create-card mt-3"
>
{t('Create Card')}
</Button>
</Popover>
</div>
)
}
>
<ListView
loading={loading}
disableSelection={!onSelectionChange}
siteId={siteId}
list={cards}
inLibrary={inLibrary}
selectedList={selectedMetrics}
// existingCardIds={existingCardIds}
toggleSelection={toggleMetricSelection}
// allSelected={cards.length === selectedMetrics.length}
// toggleAll={({ target: { checked } }) =>
// setSelectedMetrics(
// checked
// ? cards
// .map((i: any) => i.metricId)
// .slice(0, 30 - (existingCardIds?.length || 0))
// : []
// )
// }
/>
</NoContent>
); );
} }

View file

@ -10,7 +10,7 @@ import {
HEATMAP, HEATMAP,
USER_PATH, USER_PATH,
RETENTION, RETENTION,
CATEGORIES, CATEGORIES
} from 'App/constants/card'; } from 'App/constants/card';
import { clickmapFilter } from 'App/types/filter/newFilter'; import { clickmapFilter } from 'App/types/filter/newFilter';
import { getRE } from 'App/utils'; import { getRE } from 'App/utils';
@ -31,7 +31,7 @@ const handleFilter = (card: Widget, filterType?: string) => {
FilterKey.ERRORS, FilterKey.ERRORS,
FilterKey.FETCH, FilterKey.FETCH,
`${TIMESERIES}_4xx_requests`, `${TIMESERIES}_4xx_requests`,
`${TIMESERIES}_slow_network_requests`, `${TIMESERIES}_slow_network_requests`
].includes(metricOf); ].includes(metricOf);
} }
if (filterType === CATEGORIES.web_analytics) { if (filterType === CATEGORIES.web_analytics) {
@ -41,7 +41,7 @@ const handleFilter = (card: Widget, filterType?: string) => {
FilterKey.REFERRER, FilterKey.REFERRER,
FilterKey.USERID, FilterKey.USERID,
FilterKey.LOCATION, FilterKey.LOCATION,
FilterKey.USER_DEVICE, FilterKey.USER_DEVICE
].includes(metricOf); ].includes(metricOf);
} }
} else { } else {
@ -75,58 +75,42 @@ interface MetricFilter {
query?: string; query?: string;
showMine?: boolean; showMine?: boolean;
type?: string; type?: string;
dashboard?: []; // dashboard?: [];
} }
export default class MetricStore { export default class MetricStore {
isLoading: boolean = false; isLoading: boolean = false;
isSaving: boolean = false; isSaving: boolean = false;
metrics: Widget[] = []; metrics: Widget[] = [];
instance = new Widget(); instance = new Widget();
page: number = 1; page: number = 1;
total: number = 0;
pageSize: number = 10; pageSize: number = 10;
metricsSearch: string = ''; metricsSearch: string = '';
sort: any = { columnKey: '', field: '', order: false };
sort: any = { by: 'desc' }; filter: any = { type: '', query: '' };
filter: MetricFilter = { type: 'all', dashboard: [], query: '' };
sessionsPage: number = 1; sessionsPage: number = 1;
sessionsPageSize: number = 10; sessionsPageSize: number = 10;
listView?: boolean = true; listView?: boolean = true;
clickMapFilter: boolean = false; clickMapFilter: boolean = false;
clickMapSearch = ''; clickMapSearch = '';
clickMapLabel = ''; clickMapLabel = '';
cardCategory: string | null = CATEGORIES.product_analytics; cardCategory: string | null = CATEGORIES.product_analytics;
focusedSeriesName: string | null = null; focusedSeriesName: string | null = null;
disabledSeries: string[] = []; disabledSeries: string[] = [];
drillDown = false; drillDown = false;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
get sortedWidgets() { // get sortedWidgets() {
return [...this.metrics].sort((a, b) => // return [...this.metrics].sort((a, b) =>
this.sort.by === 'desc' // this.sort.by === 'desc'
? b.lastModified - a.lastModified // ? b.lastModified - a.lastModified
: a.lastModified - b.lastModified, // : a.lastModified - b.lastModified
); // );
} // }
get filteredCards() { get filteredCards() {
const filterRE = this.filter.query ? getRE(this.filter.query, 'i') : null; const filterRE = this.filter.query ? getRE(this.filter.query, 'i') : null;
@ -138,7 +122,7 @@ export default class MetricStore {
(card) => (card) =>
(this.filter.showMine (this.filter.showMine
? card.owner === ? card.owner ===
JSON.parse(localStorage.getItem('user')!).account.email JSON.parse(localStorage.getItem('user')!).account.email
: true) && : true) &&
handleFilter(card, this.filter.type) && handleFilter(card, this.filter.type) &&
(!dbIds.length || (!dbIds.length ||
@ -147,13 +131,13 @@ export default class MetricStore {
.some((id) => dbIds.includes(id))) && .some((id) => dbIds.includes(id))) &&
// @ts-ignore // @ts-ignore
(!filterRE || (!filterRE ||
['name', 'owner'].some((key) => filterRE.test(card[key]))), ['name', 'owner'].some((key) => filterRE.test(card[key])))
)
.sort((a, b) =>
this.sort.by === 'desc'
? b.lastModified - a.lastModified
: a.lastModified - b.lastModified,
); );
// .sort((a, b) =>
// this.sort.by === 'desc'
// ? b.lastModified - a.lastModified
// : a.lastModified - b.lastModified
// );
} }
// State Actions // State Actions
@ -182,6 +166,7 @@ export default class MetricStore {
} }
updateKey(key: string, value: any) { updateKey(key: string, value: any) {
console.log('key', key, value);
// @ts-ignore // @ts-ignore
this[key] = value; this[key] = value;
@ -207,7 +192,7 @@ export default class MetricStore {
this.instance.series[i].filter.eventsOrderSupport = [ this.instance.series[i].filter.eventsOrderSupport = [
'then', 'then',
'or', 'or',
'and', 'and'
]; ];
}); });
if (type === HEATMAP && 'series' in obj) { if (type === HEATMAP && 'series' in obj) {
@ -254,7 +239,7 @@ export default class MetricStore {
namesMap: {}, namesMap: {},
avg: 0, avg: 0,
percentiles: [], percentiles: [],
values: [], values: []
}; };
const obj: any = { metricType: value, data: defaultData }; const obj: any = { metricType: value, data: defaultData };
obj.series = this.instance.series; obj.series = this.instance.series;
@ -311,7 +296,7 @@ export default class MetricStore {
if (obj.series[0] && obj.series[0].filter.filters.length < 1) { if (obj.series[0] && obj.series[0].filter.filters.length < 1) {
obj.series[0].filter.addFilter({ obj.series[0].filter.addFilter({
...clickmapFilter, ...clickmapFilter,
value: [''], value: ['']
}); });
} }
} }
@ -341,7 +326,7 @@ export default class MetricStore {
updateInList(metric: Widget) { updateInList(metric: Widget) {
// @ts-ignore // @ts-ignore
const index = this.metrics.findIndex( const index = this.metrics.findIndex(
(m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY], (m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY]
); );
if (index >= 0) { if (index >= 0) {
this.metrics[index] = metric; this.metrics[index] = metric;
@ -358,12 +343,6 @@ export default class MetricStore {
this.metrics = this.metrics.filter((m) => m[Widget.ID_KEY] !== id); this.metrics = this.metrics.filter((m) => m[Widget.ID_KEY] !== id);
} }
get paginatedList(): Widget[] {
const start = (this.page - 1) * this.pageSize;
const end = start + this.pageSize;
return this.metrics.slice(start, end);
}
// API Communication // API Communication
async save(metric: Widget): Promise<Widget> { async save(metric: Widget): Promise<Widget> {
this.isSaving = true; this.isSaving = true;
@ -396,16 +375,27 @@ export default class MetricStore {
this.metrics = metrics; this.metrics = metrics;
} }
fetchList() { async fetchList() {
this.setLoading(true); this.setLoading(true);
return metricService try {
.getMetrics() const resp = await metricService
.then((metrics: any[]) => { .getMetricsPaginated({
this.setMetrics(metrics.map((m) => new Widget().fromJson(m))); page: this.page,
}) limit: this.pageSize,
.finally(() => { sort: {
this.setLoading(false); field: this.sort.field,
}); order: this.sort.order === 'ascend' ? 'asc' : 'desc'
},
filter: {
query: this.filter.query,
type: this.filter.type === 'all' ? '' : this.filter.type,
}
});
this.total = resp.total;
this.setMetrics(resp.list.map((m) => new Widget().fromJson(m)));
} finally {
this.setLoading(false);
}
} }
fetch(id: string, period?: any) { fetch(id: string, period?: any) {

View file

@ -24,6 +24,17 @@ export default class MetricService {
.then((response: { data: any }) => response.data || []); .then((response: { data: any }) => response.data || []);
} }
/**
* Get all metrics paginated.
* @returns {Promise<any>}
*/
getMetricsPaginated(params: any): Promise<any> {
return this.client
.post('/cards/search', params)
.then((response: { json: () => any }) => response.json())
.then((response: { data: any }) => response.data || []);
}
/** /**
* Get a metric by metricId. * Get a metric by metricId.
* @param metricId * @param metricId