[WEB-4323] refactor: Analytics refactor (#7213)
* chore: updated label for epics
* chore: improved export logic
* refactor: move csvConfig to export.ts and clean up export logic
* refactor: remove unused CSV export logic from WorkItemsInsightTable component
* refactor: streamline data handling in InsightTable component for improved rendering
* feat: add translation for "No. of {entity}" and update priority chart y-axis label to use new translation
* refactor: cleaned up some component and added utilitites
* feat: add "at_risk" translation to multiple languages in translations.json files
* refactor: update TrendPiece component to use new status variants for analytics
* fix: adjust TrendPiece component logic for on-track and off-track status
* refactor: use nullish coalescing operator for yAxis.dx in line and scatter charts
* feat: add "at_risk" translation to various languages in translations.json files
* feat: add "no_of" translation to various languages in translations.json files
* feat: update "at_risk" translation in Ukrainian, Vietnamese, and Chinese locales in translations.json files
* refactor: rename insightsFields to ANALYTICS_INSIGHTS_FIELDS and update analytics tab import to use getAnalyticsTabs function
* feat: update AnalyticsWrapper to use i18n for titles and add new translation for "no_of" in Russian
* fix: update yAxis labels and offsets in various charts to use new translation key and improve layout
* feat: define AnalyticsTab interface and refactor getAnalyticsTabs function for improved type safety
* fix: update AnalyticsTab interface to use TAnalyticsTabsBase for improved type safety
* fix: add whitespace-nowrap class to TableHead for improved header layout in DataTable component
This commit is contained in:
parent
6fe0415d66
commit
0fa9c8b015
14 changed files with 41 additions and 34 deletions
|
|
@ -11,7 +11,7 @@ export interface IInsightField {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const insightsFields: Record<TAnalyticsTabsBase, IInsightField[]> = {
|
export const ANALYTICS_INSIGHTS_FIELDS: Record<TAnalyticsTabsBase, IInsightField[]> = {
|
||||||
overview: [
|
overview: [
|
||||||
{
|
{
|
||||||
key: "total_users",
|
key: "total_users",
|
||||||
|
|
|
||||||
|
|
@ -881,7 +881,8 @@
|
||||||
"completed": "Завершено",
|
"completed": "Завершено",
|
||||||
"in_progress": "В процессе",
|
"in_progress": "В процессе",
|
||||||
"planned": "Запланировано",
|
"planned": "Запланировано",
|
||||||
"paused": "На паузе"
|
"paused": "На паузе",
|
||||||
|
"no_of": "Количество {entity}"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Ось X",
|
"x_axis": "Ось X",
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ export const BarChart = React.memo(<K extends string, T extends string>(props: T
|
||||||
value: yAxis.label,
|
value: yAxis.label,
|
||||||
angle: -90,
|
angle: -90,
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
offset: -24,
|
offset: yAxis.offset ?? -24,
|
||||||
dx: -16,
|
dx: yAxis.dx ?? -16,
|
||||||
className: AXIS_LABEL_CLASSNAME,
|
className: AXIS_LABEL_CLASSNAME,
|
||||||
}}
|
}}
|
||||||
tick={(props) => <CustomYAxisTick {...props} />}
|
tick={(props) => <CustomYAxisTick {...props} />}
|
||||||
|
|
|
||||||
6
packages/types/src/analytics.d.ts
vendored
6
packages/types/src/analytics.d.ts
vendored
|
|
@ -4,6 +4,12 @@ import { Row } from "@tanstack/react-table";
|
||||||
|
|
||||||
export type TAnalyticsTabsBase = "overview" | "work-items";
|
export type TAnalyticsTabsBase = "overview" | "work-items";
|
||||||
export type TAnalyticsGraphsBase = "projects" | "work-items" | "custom-work-items";
|
export type TAnalyticsGraphsBase = "projects" | "work-items" | "custom-work-items";
|
||||||
|
export interface AnalyticsTab {
|
||||||
|
key: TAnalyticsTabsBase;
|
||||||
|
label: string;
|
||||||
|
content: React.FC;
|
||||||
|
isDisabled: boolean;
|
||||||
|
}
|
||||||
export type TAnalyticsFilterParams = {
|
export type TAnalyticsFilterParams = {
|
||||||
project_ids?: string;
|
project_ids?: string;
|
||||||
cycle_id?: string;
|
cycle_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
|
||||||
// hooks
|
// hooks
|
||||||
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
|
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
|
||||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||||
import { ANALYTICS_TABS } from "@/plane-web/components/analytics/tabs";
|
import { getAnalyticsTabs } from "@/plane-web/components/analytics/tabs";
|
||||||
|
|
||||||
const AnalyticsPage = observer(() => {
|
const AnalyticsPage = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -40,17 +40,20 @@ const AnalyticsPage = observer(() => {
|
||||||
EUserPermissionsLevel.WORKSPACE
|
EUserPermissionsLevel.WORKSPACE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ANALYTICS_TABS = useMemo(() => getAnalyticsTabs(t), [t]);
|
||||||
|
|
||||||
const tabs = useMemo(
|
const tabs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
ANALYTICS_TABS.map((tab) => ({
|
ANALYTICS_TABS.map((tab) => ({
|
||||||
key: tab.key,
|
key: tab.key,
|
||||||
label: t(tab.i18nKey),
|
label: tab.label,
|
||||||
content: <tab.content />,
|
content: <tab.content />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push(`?tab=${tab.key}`);
|
router.push(`?tab=${tab.key}`);
|
||||||
},
|
},
|
||||||
|
isDisabled: tab.isDisabled,
|
||||||
})),
|
})),
|
||||||
[router, t]
|
[ANALYTICS_TABS, router]
|
||||||
);
|
);
|
||||||
const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key;
|
const defaultTab = searchParams.get("tab") || ANALYTICS_TABS[0].key;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { TAnalyticsTabsBase } from "@plane/types";
|
|
||||||
import { Overview } from "@/components/analytics/overview";
|
|
||||||
import { WorkItems } from "@/components/analytics/work-items";
|
|
||||||
export const ANALYTICS_TABS: {
|
|
||||||
key: TAnalyticsTabsBase;
|
|
||||||
i18nKey: string;
|
|
||||||
content: React.FC;
|
|
||||||
isExtended?: boolean;
|
|
||||||
}[] = [
|
|
||||||
{ key: "overview", i18nKey: "common.overview", content: Overview },
|
|
||||||
{ key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems },
|
|
||||||
];
|
|
||||||
8
web/ce/components/analytics/tabs.tsx
Normal file
8
web/ce/components/analytics/tabs.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { AnalyticsTab } from "@plane/types";
|
||||||
|
import { Overview } from "@/components/analytics/overview";
|
||||||
|
import { WorkItems } from "@/components/analytics/work-items";
|
||||||
|
|
||||||
|
export const getAnalyticsTabs = (t: (key: string, params?: Record<string, any>) => string): AnalyticsTab[] => [
|
||||||
|
{ key: "overview", label: t("common.overview"), content: Overview, isDisabled: false },
|
||||||
|
{ key: "work-items", label: t("sidebar.work_items"), content: WorkItems, isDisabled: false },
|
||||||
|
];
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// plane package imports
|
// plane package imports
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
i18nTitle: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnalyticsWrapper: React.FC<Props> = (props) => {
|
const AnalyticsWrapper: React.FC<Props> = (props) => {
|
||||||
const { title, children, className } = props;
|
const { i18nTitle, children, className } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className={cn("px-6 py-4", className)}>
|
<div className={cn("px-6 py-4", className)}>
|
||||||
<h1 className={"mb-4 text-2xl font-bold md:mb-6"}>{title}</h1>
|
<h1 className={"mb-4 text-2xl font-bold md:mb-6"}>{t(i18nTitle)}</h1>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
<TableHead key={header.id} colSpan={header.colSpan} className="whitespace-nowrap">
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: (flexRender(header.column.columnDef.header, header.getContext()) as any)}
|
: (flexRender(header.column.columnDef.header, header.getContext()) as any)}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import ActiveProjects from "./active-projects";
|
||||||
import ProjectInsights from "./project-insights";
|
import ProjectInsights from "./project-insights";
|
||||||
|
|
||||||
const Overview: React.FC = () => (
|
const Overview: React.FC = () => (
|
||||||
<AnalyticsWrapper title="Overview">
|
<AnalyticsWrapper i18nTitle="common.overview">
|
||||||
<div className="flex flex-col gap-14">
|
<div className="flex flex-col gap-14">
|
||||||
<TotalInsights analyticsType="overview" />
|
<TotalInsights analyticsType="overview" />
|
||||||
<div className="grid grid-cols-1 gap-14 md:grid-cols-5 ">
|
<div className="grid grid-cols-1 gap-14 md:grid-cols-5 ">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { IInsightField, insightsFields } from "@plane/constants";
|
import { IInsightField, ANALYTICS_INSIGHTS_FIELDS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { IAnalyticsResponse, TAnalyticsTabsBase } from "@plane/types";
|
import { IAnalyticsResponse, TAnalyticsTabsBase } from "@plane/types";
|
||||||
//hooks
|
//hooks
|
||||||
|
|
@ -80,13 +80,13 @@ const TotalInsights: React.FC<{
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid grid-cols-1 gap-8 sm:grid-cols-2 md:gap-10",
|
"grid grid-cols-1 gap-8 sm:grid-cols-2 md:gap-10",
|
||||||
!peekView
|
!peekView
|
||||||
? insightsFields[analyticsType]?.length % 5 === 0
|
? ANALYTICS_INSIGHTS_FIELDS[analyticsType]?.length % 5 === 0
|
||||||
? "gap-10 lg:grid-cols-5"
|
? "gap-10 lg:grid-cols-5"
|
||||||
: "gap-8 lg:grid-cols-4"
|
: "gap-8 lg:grid-cols-4"
|
||||||
: "grid-cols-2"
|
: "grid-cols-2"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{insightsFields[analyticsType]?.map((item) => (
|
{ANALYTICS_INSIGHTS_FIELDS[analyticsType]?.map((item) => (
|
||||||
<InsightCard
|
<InsightCard
|
||||||
key={`${analyticsType}-${item.key}`}
|
key={`${analyticsType}-${item.key}`}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,9 @@ const CreatedVsResolved = observer(() => {
|
||||||
}}
|
}}
|
||||||
yAxis={{
|
yAxis={{
|
||||||
key: "count",
|
key: "count",
|
||||||
label: t("no_of", { entity: isEpic ? t("epics") : t("work_items") }),
|
label: t("common.no_of", { entity: isEpic ? t("epics") : t("work_items") }),
|
||||||
offset: -30,
|
offset: -60,
|
||||||
dx: -22,
|
dx: -24,
|
||||||
}}
|
}}
|
||||||
legend={{
|
legend={{
|
||||||
align: "left",
|
align: "left",
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,8 @@ const PriorityChart = observer((props: Props) => {
|
||||||
}}
|
}}
|
||||||
yAxis={{
|
yAxis={{
|
||||||
key: "count",
|
key: "count",
|
||||||
label: t("no_of", { entity: yAxisLabel.replace("_", " ") }),
|
label: t("common.no_of", { entity: yAxisLabel.replace("_", " ") }),
|
||||||
offset: -40,
|
offset: -60,
|
||||||
dx: -26,
|
dx: -26,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import CustomizedInsights from "./customized-insights";
|
||||||
import WorkItemsInsightTable from "./workitems-insight-table";
|
import WorkItemsInsightTable from "./workitems-insight-table";
|
||||||
|
|
||||||
const WorkItems: React.FC = () => (
|
const WorkItems: React.FC = () => (
|
||||||
<AnalyticsWrapper title="Work Items">
|
<AnalyticsWrapper i18nTitle="sidebar.work_items">
|
||||||
<div className="flex flex-col gap-14">
|
<div className="flex flex-col gap-14">
|
||||||
<TotalInsights analyticsType="work-items" />
|
<TotalInsights analyticsType="work-items" />
|
||||||
<CreatedVsResolved />
|
<CreatedVsResolved />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue