Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions packages/app-builder/src/components/Analytics/Decisions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Spinner } from '@app-builder/components/Spinner';
import { useResizeObserver } from '@app-builder/hooks/useResizeObserver';
import {
type DecisionOutcomesAbsolute,
DecisionOutcomesPerPeriod,
type DecisionsFilter,
type Outcome,
outcomeColors,
type RangeId,
} from '@app-builder/models/analytics';
Expand Down Expand Up @@ -31,9 +34,10 @@ export type DecisionsPerOutcome = {
interface DecisionsProps {
data: DecisionOutcomesPerPeriod | null;
scenarioVersions: { version: number; createdAt: string }[];
isLoading?: boolean;
}

export function Decisions({ data, scenarioVersions }: DecisionsProps) {
export function Decisions({ data, scenarioVersions, isLoading = false }: DecisionsProps) {
const { t } = useTranslation();
const language = useFormatLanguage();

Expand Down Expand Up @@ -174,20 +178,71 @@ export function Decisions({ data, scenarioVersions }: DecisionsProps) {
return currentDataGroup?.gridXValues;
};

const handleExportCsv = () => {
if (!currentDataGroup) return;
const rows = currentDataGroup.data.absolute;
if (!rows.length) return;

const selectedOutcomes: Outcome[] = Array.from(decisions.entries())
.filter(([, value]) => value)
.map(([key]) => key);

const headers = ['date', 'rangeId', ...selectedOutcomes, ['total']];

const lines = rows.map((row) => {
const base = [row.date, row.rangeId];
type OutcomeValues = Pick<DecisionsPerOutcome, Outcome>;
const outcomeValues = selectedOutcomes.map((k) => {
const v = (row as OutcomeValues)[k];
return String(v);
});
const maybeTotal = [String((row as DecisionOutcomesAbsolute).total ?? 0)];
return [...base, ...outcomeValues, ...maybeTotal].join(',');
});

const csv = [headers.join(','), ...lines].join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8,' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `decisions_${groupDate}_${percentage ? 'percentage' : 'absolute'}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
};

return (
<div>
<div className="flex items-center justify-between">
<h2 className="text-l font-semibold">{t('analytics:decisions.title')}</h2>
<ButtonV2 variant="secondary" className="flex items-center gap-v2-sm" disabled={true}>
<h2 className="text-h2 font-semibold">{t('analytics:decisions.title')}</h2>
<ButtonV2
variant="secondary"
className="flex items-center gap-v2-sm"
disabled={
isLoading ||
!currentDataGroup ||
(percentage
? (currentDataGroup.data.ratio?.length ?? 0) === 0
: (currentDataGroup.data.absolute?.length ?? 0) === 0)
}
onClick={handleExportCsv}
>
<Icon icon="download" className="size-4" />
{t('analytics:decisions.export.button')}
</ButtonV2>
</div>

<div
ref={divRef}
className="bg-white border border-grey-90 rounded-lg p-v2-md shadow-sm mt-v2-sm"
aria-busy={isLoading}
className="bg-white border border-grey-90 rounded-lg p-v2-md shadow-sm mt-v2-sm relative"
>
{isLoading ? (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-grey-98/80 hover:bg-grey-95/80">
<Spinner className="size-6" />
</div>
) : null}
<div className="flex w-full h-[500px] flex-col items-start gap-v2-md">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-v2-sm">
Expand Down
127 changes: 0 additions & 127 deletions packages/app-builder/src/components/Analytics/Filters.tsx

This file was deleted.

114 changes: 114 additions & 0 deletions packages/app-builder/src/components/Analytics/RulesHit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Spinner } from '@app-builder/components/Spinner';
import { type RuleHitTableResponse } from '@app-builder/models/analytics/rule-hit';
import { formatNumber, useFormatLanguage } from '@app-builder/utils/format';
import { createColumnHelper, getCoreRowModel } from '@tanstack/react-table';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Table, useTable } from 'ui-design-system';

export function RulesHit({
data,
isLoading,
}: {
data: RuleHitTableResponse[];
isLoading: boolean;
}) {
const { t } = useTranslation(['analytics']);
const language = useFormatLanguage();
const [expanded, setExpanded] = useState(false);

const visibleData = useMemo(() => (expanded ? data : data.slice(0, 5)), [expanded, data]);

const columnHelper = createColumnHelper<RuleHitTableResponse>();
const toPercent = (value: number) =>
formatNumber(value > 1 ? value / 100 : value, {
language,
style: 'percent',
maximumFractionDigits: 1,
});

const columns = useMemo(
() => [
columnHelper.accessor((row) => row.ruleName, {
id: 'rule',
header: t('analytics:ruleshit.columns.rule'),
size: 220,
cell: ({ getValue }) => <span className="line-clamp-1">{getValue()}</span>,
}),
columnHelper.accessor((row) => row.hitCount, {
id: 'hitCount',
header: t('analytics:ruleshit.columns.hit_count'),
size: 100,
cell: ({ getValue }) => <span>{formatNumber(getValue(), { language })}</span>,
}),
columnHelper.accessor((row) => row.hitRatio, {
id: 'hitRatio',
header: t('analytics:ruleshit.columns.hit_ratio'),
size: 120,
cell: ({ getValue }) => <span>{toPercent(getValue())}</span>,
}),
columnHelper.accessor((row) => row.pivotCount, {
id: 'pivotCount',
header: t('analytics:ruleshit.columns.pivot_count'),
size: 140,
cell: ({ getValue }) => <span>{formatNumber(getValue(), { language })}</span>,
}),
columnHelper.accessor((row) => row.pivotRatio, {
id: 'pivotRatio',
header: t('analytics:ruleshit.columns.pivot_ratio'),
size: 160,
cell: ({ getValue }) => <span>{toPercent(getValue())}</span>,
}),
],
[columnHelper, language, t],
);

const { table, getBodyProps, rows, getContainerProps } = useTable({
data: visibleData,
columns,
columnResizeMode: 'onChange',
getCoreRowModel: getCoreRowModel(),
enableSorting: false,
});
return (
<div className="mt-v2-xl">
<div className="flex items-center justify-between">
<h2 className="text-h2 font-semibold">{t('analytics:ruleshit.title')}</h2>
</div>

<div
aria-busy={isLoading}
className="bg-white border border-grey-90 rounded-lg p-v2-md shadow-sm mt-v2-sm relative"
>
{isLoading ? (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-grey-98/80 hover:bg-grey-95/80">
<Spinner className="size-6" />
</div>
) : null}
<div className="flex w-full flex-col items-start gap-v2-md">
<Table.Container {...getContainerProps()} className="bg-grey-100 w-full">
<Table.Header headerGroups={table.getHeaderGroups()} />
<Table.Body {...getBodyProps()}>
{rows.map((row) => (
<Table.Row key={row.id} row={row} />
))}
{!expanded && data.length > 5 ? (
<tr
className="even:bg-grey-98 h-12 hover:bg-purple-98 cursor-pointer"
onClick={() => setExpanded(true)}
>
<td
className="text-s w-full truncate px-4 font-medium text-purple-65"
colSpan={table.getHeaderGroups()[0]?.headers.length ?? 5}
>
{t('analytics:ruleshit.see_more.label')}
</td>
</tr>
) : null}
</Table.Body>
</Table.Container>
</div>
</div>
</div>
);
}
Loading