[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:
JayashTripathy 2025-06-16 14:01:49 +05:30 committed by GitHub
parent 6fe0415d66
commit 0fa9c8b015
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 41 additions and 34 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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} />}

View file

@ -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;

View file

@ -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;

View file

@ -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 },
];

View 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 },
];

View file

@ -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>
); );

View file

@ -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)}

View file

@ -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 ">

View file

@ -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}

View file

@ -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",

View file

@ -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,
}} }}
/> />

View file

@ -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 />