[WEB-4230] refactor: Analytics code refacor, Removal of nivo charts dependencies and translations (#7131)
* chore: added code split for the analytics store * chore: done some refactor * refactor: update entity keys in analytics and translations * chore: updated the translations * refactor: simplify AnalyticsStoreV2 class by removing unnecessary constructor * feat: add AnalyticsStoreV2 class and interface for enhanced analytics functionality * feat: enhance WorkItemsModal and analytics store with isEpic functionality * feat: integrate isEpic state into TotalInsights and WorkItemsModal components * refactor: remove isEpic state from WorkItemsModalMainContent component * refactor: removed old analytics components and related services * refactor: new analytics * refactor: removed all nivo chart dependencies * chore: resolved coderabbit comments * fix: update processUrl to handle custom-work-items in peek view * feat: implement CSV export functionality in InsightTable component * feat: enhance analytics service with filter parameters and improve data handling in InsightTable * feat: add new translation keys for various statuses across multiple languages * [WEB-4246] fix: enhance analytics components to include 'isEpic' parameter for improved data fetching * chore: update yarn.lock to remove deprecated @nivo packages and clean up unused dependencies
This commit is contained in:
parent
570a9e319e
commit
14d2d69120
151 changed files with 1144 additions and 4800 deletions
|
|
@ -1,105 +0,0 @@
|
||||||
import { TAnalyticsTabsV2Base } from "@plane/types";
|
|
||||||
import { ChartXAxisProperty, ChartYAxisMetric } from "../chart";
|
|
||||||
|
|
||||||
export const insightsFields: Record<TAnalyticsTabsV2Base, string[]> = {
|
|
||||||
overview: [
|
|
||||||
"total_users",
|
|
||||||
"total_admins",
|
|
||||||
"total_members",
|
|
||||||
"total_guests",
|
|
||||||
"total_projects",
|
|
||||||
"total_work_items",
|
|
||||||
"total_cycles",
|
|
||||||
"total_intake",
|
|
||||||
],
|
|
||||||
"work-items": [
|
|
||||||
"total_work_items",
|
|
||||||
"started_work_items",
|
|
||||||
"backlog_work_items",
|
|
||||||
"un_started_work_items",
|
|
||||||
"completed_work_items",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ANALYTICS_V2_DURATION_FILTER_OPTIONS = [
|
|
||||||
{
|
|
||||||
name: "Yesterday",
|
|
||||||
value: "yesterday",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Last 7 days",
|
|
||||||
value: "last_7_days",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Last 30 days",
|
|
||||||
value: "last_30_days",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Last 3 months",
|
|
||||||
value: "last_3_months",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ANALYTICS_V2_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.STATES,
|
|
||||||
label: "State name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.STATE_GROUPS,
|
|
||||||
label: "State group",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.PRIORITY,
|
|
||||||
label: "Priority",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.LABELS,
|
|
||||||
label: "Label",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.ASSIGNEES,
|
|
||||||
label: "Assignee",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.ESTIMATE_POINTS,
|
|
||||||
label: "Estimate point",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.CYCLES,
|
|
||||||
label: "Cycle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.MODULES,
|
|
||||||
label: "Module",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.COMPLETED_AT,
|
|
||||||
label: "Completed date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.TARGET_DATE,
|
|
||||||
label: "Due date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.START_DATE,
|
|
||||||
label: "Start date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartXAxisProperty.CREATED_AT,
|
|
||||||
label: "Created date",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ANALYTICS_V2_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [
|
|
||||||
{
|
|
||||||
value: ChartYAxisMetric.WORK_ITEM_COUNT,
|
|
||||||
label: "Work item",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
|
||||||
label: "Estimate",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"];
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
// types
|
|
||||||
import { TXAxisValues, TYAxisValues } from "@plane/types";
|
|
||||||
|
|
||||||
export const ANALYTICS_TABS = [
|
|
||||||
{
|
|
||||||
key: "scope_and_demand",
|
|
||||||
i18n_title: "workspace_analytics.tabs.scope_and_demand",
|
|
||||||
},
|
|
||||||
{ key: "custom", i18n_title: "workspace_analytics.tabs.custom" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
|
|
||||||
[
|
|
||||||
{
|
|
||||||
value: "state_id",
|
|
||||||
label: "State name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "state__group",
|
|
||||||
label: "State group",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "priority",
|
|
||||||
label: "Priority",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "labels__id",
|
|
||||||
label: "Label",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "assignees__id",
|
|
||||||
label: "Assignee",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "estimate_point__value",
|
|
||||||
label: "Estimate point",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "issue_cycle__cycle_id",
|
|
||||||
label: "Cycle",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "issue_module__module_id",
|
|
||||||
label: "Module",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "completed_at",
|
|
||||||
label: "Completed date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "target_date",
|
|
||||||
label: "Due date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "start_date",
|
|
||||||
label: "Start date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "created_at",
|
|
||||||
label: "Created date",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
|
|
||||||
[
|
|
||||||
{
|
|
||||||
value: "issue_count",
|
|
||||||
label: "Work item Count",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "estimate",
|
|
||||||
label: "Estimate",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ANALYTICS_DATE_KEYS = [
|
|
||||||
"completed_at",
|
|
||||||
"target_date",
|
|
||||||
"start_date",
|
|
||||||
"created_at",
|
|
||||||
];
|
|
||||||
178
packages/constants/src/analytics/common.ts
Normal file
178
packages/constants/src/analytics/common.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { TAnalyticsTabsBase } from "@plane/types";
|
||||||
|
import { ChartXAxisProperty, ChartYAxisMetric } from "../chart";
|
||||||
|
|
||||||
|
export interface IInsightField {
|
||||||
|
key: string;
|
||||||
|
i18nKey: string;
|
||||||
|
i18nProps?: {
|
||||||
|
entity?: string;
|
||||||
|
entityPlural?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insightsFields: Record<TAnalyticsTabsBase, IInsightField[]> = {
|
||||||
|
overview: [
|
||||||
|
{
|
||||||
|
key: "total_users",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "common.users",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total_admins",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "common.admins",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total_members",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "common.members",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total_guests",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "common.guests",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total_projects",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "common.projects",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total_work_items",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "common.work_items",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total_cycles",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "common.cycles",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "total_intake",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
i18nProps: {
|
||||||
|
entity: "sidebar.intake",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"work-items": [
|
||||||
|
{
|
||||||
|
key: "total_work_items",
|
||||||
|
i18nKey: "workspace_analytics.total",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "started_work_items",
|
||||||
|
i18nKey: "workspace_analytics.started_work_items",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "backlog_work_items",
|
||||||
|
i18nKey: "workspace_analytics.backlog_work_items",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "un_started_work_items",
|
||||||
|
i18nKey: "workspace_analytics.un_started_work_items",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "completed_work_items",
|
||||||
|
i18nKey: "workspace_analytics.completed_work_items",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ANALYTICS_DURATION_FILTER_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: "Yesterday",
|
||||||
|
value: "yesterday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Last 7 days",
|
||||||
|
value: "last_7_days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Last 30 days",
|
||||||
|
value: "last_30_days",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Last 3 months",
|
||||||
|
value: "last_3_months",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ANALYTICS_X_AXIS_VALUES: { value: ChartXAxisProperty; label: string }[] = [
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.STATES,
|
||||||
|
label: "State name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.STATE_GROUPS,
|
||||||
|
label: "State group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.PRIORITY,
|
||||||
|
label: "Priority",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.LABELS,
|
||||||
|
label: "Label",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.ASSIGNEES,
|
||||||
|
label: "Assignee",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.ESTIMATE_POINTS,
|
||||||
|
label: "Estimate point",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.CYCLES,
|
||||||
|
label: "Cycle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.MODULES,
|
||||||
|
label: "Module",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.COMPLETED_AT,
|
||||||
|
label: "Completed date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.TARGET_DATE,
|
||||||
|
label: "Due date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.START_DATE,
|
||||||
|
label: "Start date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartXAxisProperty.CREATED_AT,
|
||||||
|
label: "Created date",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ANALYTICS_Y_AXIS_VALUES: { value: ChartYAxisMetric; label: string }[] = [
|
||||||
|
{
|
||||||
|
value: ChartYAxisMetric.WORK_ITEM_COUNT,
|
||||||
|
label: "Work item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChartYAxisMetric.ESTIMATE_POINT_COUNT,
|
||||||
|
label: "Estimate",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ANALYTICS_V2_DATE_KEYS = ["completed_at", "target_date", "start_date", "created_at"];
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export * from "./ai";
|
export * from "./ai";
|
||||||
export * from "./analytics";
|
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./chart";
|
export * from "./chart";
|
||||||
export * from "./endpoints";
|
export * from "./endpoints";
|
||||||
|
|
@ -34,4 +33,4 @@ export * from "./emoji";
|
||||||
export * from "./subscription";
|
export * from "./subscription";
|
||||||
export * from "./settings";
|
export * from "./settings";
|
||||||
export * from "./icon";
|
export * from "./icon";
|
||||||
export * from "./analytics-v2";
|
export * from "./analytics";
|
||||||
|
|
|
||||||
|
|
@ -866,7 +866,19 @@
|
||||||
"view": "Pohled",
|
"view": "Pohled",
|
||||||
"deactivated_user": "Deaktivovaný uživatel",
|
"deactivated_user": "Deaktivovaný uživatel",
|
||||||
"apply": "Použít",
|
"apply": "Použít",
|
||||||
"applying": "Používání"
|
"applying": "Používání",
|
||||||
|
"users": "Uživatelé",
|
||||||
|
"admins": "Administrátoři",
|
||||||
|
"guests": "Hosté",
|
||||||
|
"on_track": "Na správné cestě",
|
||||||
|
"off_track": "Mimo plán",
|
||||||
|
"timeline": "Časová osa",
|
||||||
|
"completion": "Dokončení",
|
||||||
|
"upcoming": "Nadcházející",
|
||||||
|
"completed": "Dokončeno",
|
||||||
|
"in_progress": "Probíhá",
|
||||||
|
"planned": "Plánováno",
|
||||||
|
"paused": "Pozastaveno"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Osa X",
|
"x_axis": "Osa X",
|
||||||
|
|
@ -1316,19 +1328,6 @@
|
||||||
"custom": "Vlastní analytika"
|
"custom": "Vlastní analytika"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Sledujte pokrok, vytížení a alokace. Identifikujte trendy, odstraňte překážky a zrychlete práci",
|
|
||||||
"description": "Sledujte rozsah vs. poptávku, odhady a rozsah. Zjistěte výkonnost členů a týmů, zajistěte včasné dokončení projektů.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Začněte první projekt",
|
|
||||||
"comic": {
|
|
||||||
"title": "Analytika funguje nejlépe s Cykly + Moduly",
|
|
||||||
"description": "Nejprve časově ohraničte práci do Cyklů a seskupte položky přesahující cyklus do Modulů. Najdete je v levém menu."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Pracovní položky přiřazené vám, rozdělené podle stavu, se zde zobrazí.",
|
"description": "Pracovní položky přiřazené vám, rozdělené podle stavu, se zde zobrazí.",
|
||||||
"title": "Zatím žádná data"
|
"title": "Zatím žádná data"
|
||||||
|
|
@ -1344,21 +1343,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Vytvořeno vs Vyřešeno",
|
"created_vs_resolved": "Vytvořeno vs Vyřešeno",
|
||||||
"customized_insights": "Přizpůsobené přehledy",
|
"customized_insights": "Přizpůsobené přehledy",
|
||||||
"backlog_work_items": "Pracovní položky v backlogu",
|
"backlog_work_items": "Backlog {entity}",
|
||||||
"active_projects": "Aktivní projekty",
|
"active_projects": "Aktivní projekty",
|
||||||
"trend_on_charts": "Trend na grafech",
|
"trend_on_charts": "Trend na grafech",
|
||||||
"all_projects": "Všechny projekty",
|
"all_projects": "Všechny projekty",
|
||||||
"summary_of_projects": "Souhrn projektů",
|
"summary_of_projects": "Souhrn projektů",
|
||||||
"project_insights": "Přehled projektu",
|
"project_insights": "Přehled projektu",
|
||||||
"started_work_items": "Zahájené pracovní položky",
|
"started_work_items": "Zahájené {entity}",
|
||||||
"total_work_items": "Celkový počet pracovních položek",
|
"total_work_items": "Celkový počet {entity}",
|
||||||
"total_projects": "Celkový počet projektů",
|
"total_projects": "Celkový počet projektů",
|
||||||
"total_admins": "Celkový počet administrátorů",
|
"total_admins": "Celkový počet administrátorů",
|
||||||
"total_users": "Celkový počet uživatelů",
|
"total_users": "Celkový počet uživatelů",
|
||||||
"total_intake": "Celkový příjem",
|
"total_intake": "Celkový příjem",
|
||||||
"un_started_work_items": "Nezahájené pracovní položky",
|
"un_started_work_items": "Nezahájené {entity}",
|
||||||
"total_guests": "Celkový počet hostů",
|
"total_guests": "Celkový počet hostů",
|
||||||
"completed_work_items": "Dokončené pracovní položky"
|
"completed_work_items": "Dokončené {entity}",
|
||||||
|
"total": "Celkový počet {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektů}}",
|
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektů}}",
|
||||||
|
|
|
||||||
|
|
@ -866,7 +866,19 @@
|
||||||
"view": "Ansicht",
|
"view": "Ansicht",
|
||||||
"deactivated_user": "Deaktivierter Benutzer",
|
"deactivated_user": "Deaktivierter Benutzer",
|
||||||
"apply": "Anwenden",
|
"apply": "Anwenden",
|
||||||
"applying": "Wird angewendet"
|
"applying": "Wird angewendet",
|
||||||
|
"users": "Benutzer",
|
||||||
|
"admins": "Administratoren",
|
||||||
|
"guests": "Gäste",
|
||||||
|
"on_track": "Im Plan",
|
||||||
|
"off_track": "Außer Plan",
|
||||||
|
"timeline": "Zeitleiste",
|
||||||
|
"completion": "Fertigstellung",
|
||||||
|
"upcoming": "Bevorstehend",
|
||||||
|
"completed": "Abgeschlossen",
|
||||||
|
"in_progress": "In Bearbeitung",
|
||||||
|
"planned": "Geplant",
|
||||||
|
"paused": "Pausiert"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "X-Achse",
|
"x_axis": "X-Achse",
|
||||||
|
|
@ -1316,19 +1328,6 @@
|
||||||
"custom": "Benutzerdefinierte Analysen"
|
"custom": "Benutzerdefinierte Analysen"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Verfolgen Sie Fortschritt, Auslastung und Zuordnungen. Erkennen Sie Trends, entfernen Sie Blocker und beschleunigen Sie die Arbeit",
|
|
||||||
"description": "Behalten Sie Umfang vs. Nachfrage, Schätzungen und Umfang im Blick. Verfolgen Sie die Leistung von Mitgliedern und Teams, um sicherzustellen, dass Projekte pünktlich abgeschlossen werden.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Erstes Projekt starten",
|
|
||||||
"comic": {
|
|
||||||
"title": "Analysen funktionieren am besten mit Zyklen + Modulen",
|
|
||||||
"description": "Begrenzen Sie zuerst Arbeit zeitlich in Zyklen und gruppieren Sie die übergreifenden Elemente in Module. Sie finden sie im linken Menü."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Ihnen zugewiesene Arbeitselemente, aufgeschlüsselt nach Status, werden hier angezeigt.",
|
"description": "Ihnen zugewiesene Arbeitselemente, aufgeschlüsselt nach Status, werden hier angezeigt.",
|
||||||
"title": "Noch keine Daten"
|
"title": "Noch keine Daten"
|
||||||
|
|
@ -1344,21 +1343,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Erstellt vs Gelöst",
|
"created_vs_resolved": "Erstellt vs Gelöst",
|
||||||
"customized_insights": "Individuelle Einblicke",
|
"customized_insights": "Individuelle Einblicke",
|
||||||
"backlog_work_items": "Backlog-Arbeitselemente",
|
"backlog_work_items": "Backlog-{entity}",
|
||||||
"active_projects": "Aktive Projekte",
|
"active_projects": "Aktive Projekte",
|
||||||
"trend_on_charts": "Trend in Diagrammen",
|
"trend_on_charts": "Trend in Diagrammen",
|
||||||
"all_projects": "Alle Projekte",
|
"all_projects": "Alle Projekte",
|
||||||
"summary_of_projects": "Projektübersicht",
|
"summary_of_projects": "Projektübersicht",
|
||||||
"project_insights": "Projekteinblicke",
|
"project_insights": "Projekteinblicke",
|
||||||
"started_work_items": "Begonnene Arbeitselemente",
|
"started_work_items": "Begonnene {entity}",
|
||||||
"total_work_items": "Gesamte Arbeitselemente",
|
"total_work_items": "Gesamte {entity}",
|
||||||
"total_projects": "Gesamtprojekte",
|
"total_projects": "Gesamtprojekte",
|
||||||
"total_admins": "Gesamtanzahl der Admins",
|
"total_admins": "Gesamtanzahl der Admins",
|
||||||
"total_users": "Gesamtanzahl der Benutzer",
|
"total_users": "Gesamtanzahl der Benutzer",
|
||||||
"total_intake": "Gesamteinnahmen",
|
"total_intake": "Gesamteinnahmen",
|
||||||
"un_started_work_items": "Nicht begonnene Arbeitselemente",
|
"un_started_work_items": "Nicht begonnene {entity}",
|
||||||
"total_guests": "Gesamtanzahl der Gäste",
|
"total_guests": "Gesamtanzahl der Gäste",
|
||||||
"completed_work_items": "Abgeschlossene Arbeitselemente"
|
"completed_work_items": "Abgeschlossene {entity}",
|
||||||
|
"total": "Gesamte {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Projekt} few {Projekte} other {Projekte}}",
|
"label": "{count, plural, one {Projekt} few {Projekte} other {Projekte}}",
|
||||||
|
|
@ -2466,4 +2466,4 @@
|
||||||
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane ist nicht gestartet. Dies könnte daran liegen, dass einer oder mehrere Plane-Services nicht starten konnten.",
|
"plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane ist nicht gestartet. Dies könnte daran liegen, dass einer oder mehrere Plane-Services nicht starten konnten.",
|
||||||
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wählen Sie View Logs aus setup.sh und Docker-Logs, um sicherzugehen."
|
"choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wählen Sie View Logs aus setup.sh und Docker-Logs, um sicherzugehen."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -476,6 +476,9 @@
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"labels": "Labels",
|
"labels": "Labels",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
|
"admins": "Admins",
|
||||||
|
"users": "Users",
|
||||||
|
"guests": "Guests",
|
||||||
"assignees": "Assignees",
|
"assignees": "Assignees",
|
||||||
"assignee": "Assignee",
|
"assignee": "Assignee",
|
||||||
"created_by": "Created by",
|
"created_by": "Created by",
|
||||||
|
|
@ -612,6 +615,15 @@
|
||||||
"quarter": "Quarter",
|
"quarter": "Quarter",
|
||||||
"press_for_commands": "Press '/' for commands",
|
"press_for_commands": "Press '/' for commands",
|
||||||
"click_to_add_description": "Click to add description",
|
"click_to_add_description": "Click to add description",
|
||||||
|
"on_track": "On-Track",
|
||||||
|
"off_track": "Off-Track",
|
||||||
|
"timeline": "Timeline",
|
||||||
|
"completion": "Completion",
|
||||||
|
"upcoming": "Upcoming",
|
||||||
|
"completed": "Completed",
|
||||||
|
"in_progress": "In progress",
|
||||||
|
"planned": "Planned",
|
||||||
|
"paused": "Paused",
|
||||||
"search": {
|
"search": {
|
||||||
"label": "Search",
|
"label": "Search",
|
||||||
"placeholder": "Type to search",
|
"placeholder": "Type to search",
|
||||||
|
|
@ -1158,29 +1170,11 @@
|
||||||
"scope_and_demand": "Scope and Demand",
|
"scope_and_demand": "Scope and Demand",
|
||||||
"custom": "Custom Analytics"
|
"custom": "Custom Analytics"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"total": "Total {entity}",
|
||||||
"general": {
|
"started_work_items": "Started {entity}",
|
||||||
"title": "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster",
|
"backlog_work_items": "Backlog {entity}",
|
||||||
"description": "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.",
|
"un_started_work_items": "Unstarted {entity}",
|
||||||
"primary_button": {
|
"completed_work_items": "Completed {entity}",
|
||||||
"text": "Start your first project",
|
|
||||||
"comic": {
|
|
||||||
"title": "Analytics works best with Cycles + Modules",
|
|
||||||
"description": "First, timebox your work items into Cycles and, if you can, group work items that span more than a cycle into Modules. Check out both on the left nav."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"total_work_items": "Total work items",
|
|
||||||
"started_work_items": "Started work items",
|
|
||||||
"backlog_work_items": "Backlog work items",
|
|
||||||
"un_started_work_items": "Unstarted work items",
|
|
||||||
"completed_work_items": "Completed work items",
|
|
||||||
"total_guests": "Total Guests",
|
|
||||||
"total_intake": "Total Intake",
|
|
||||||
"total_users": "Total Users",
|
|
||||||
"total_admins": "Total Admins",
|
|
||||||
"total_projects": "Total Projects",
|
|
||||||
"project_insights": "Project Insights",
|
"project_insights": "Project Insights",
|
||||||
"summary_of_projects": "Summary of Projects",
|
"summary_of_projects": "Summary of Projects",
|
||||||
"all_projects": "All Projects",
|
"all_projects": "All Projects",
|
||||||
|
|
@ -1188,7 +1182,7 @@
|
||||||
"active_projects": "Active Projects",
|
"active_projects": "Active Projects",
|
||||||
"customized_insights": "Customized Insights",
|
"customized_insights": "Customized Insights",
|
||||||
"created_vs_resolved": "Created vs Resolved",
|
"created_vs_resolved": "Created vs Resolved",
|
||||||
"empty_state_v2": {
|
"empty_state": {
|
||||||
"project_insights": {
|
"project_insights": {
|
||||||
"title": "No data yet",
|
"title": "No data yet",
|
||||||
"description": "Work items assigned to you, broken down by state, will show up here."
|
"description": "Work items assigned to you, broken down by state, will show up here."
|
||||||
|
|
@ -1312,23 +1306,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"account_settings": {
|
"account_settings": {
|
||||||
"profile":{},
|
"profile": {},
|
||||||
"preferences":{
|
"preferences": {
|
||||||
"heading": "Preferences",
|
"heading": "Preferences",
|
||||||
"description": "Customize your app experience the way you work"
|
"description": "Customize your app experience the way you work"
|
||||||
},
|
},
|
||||||
"notifications":{
|
"notifications": {
|
||||||
"heading": "Email notifications",
|
"heading": "Email notifications",
|
||||||
"description": "Stay in the loop on Work items you are subscribed to. Enable this to get notified."
|
"description": "Stay in the loop on Work items you are subscribed to. Enable this to get notified."
|
||||||
},
|
},
|
||||||
"security":{
|
"security": {
|
||||||
"heading": "Security"
|
"heading": "Security"
|
||||||
},
|
},
|
||||||
"api_tokens":{
|
"api_tokens": {
|
||||||
"heading": "Personal Access Tokens",
|
"heading": "Personal Access Tokens",
|
||||||
"description": "Generate secure API tokens to integrate your data with external systems and applications."
|
"description": "Generate secure API tokens to integrate your data with external systems and applications."
|
||||||
},
|
},
|
||||||
"activity":{
|
"activity": {
|
||||||
"heading": "Activity",
|
"heading": "Activity",
|
||||||
"description": "Track your recent actions and changes across all projects and work items."
|
"description": "Track your recent actions and changes across all projects and work items."
|
||||||
}
|
}
|
||||||
|
|
@ -1400,7 +1394,7 @@
|
||||||
},
|
},
|
||||||
"billing_and_plans": {
|
"billing_and_plans": {
|
||||||
"heading": "Billing & Plans",
|
"heading": "Billing & Plans",
|
||||||
"description":"Choose your plan, manage subscriptions, and easily upgrade as your needs grow.",
|
"description": "Choose your plan, manage subscriptions, and easily upgrade as your needs grow.",
|
||||||
"title": "Billing & Plans",
|
"title": "Billing & Plans",
|
||||||
"current_plan": "Current plan",
|
"current_plan": "Current plan",
|
||||||
"free_plan": "You are currently using the free plan",
|
"free_plan": "You are currently using the free plan",
|
||||||
|
|
|
||||||
|
|
@ -869,7 +869,19 @@
|
||||||
"view": "Ver",
|
"view": "Ver",
|
||||||
"deactivated_user": "Usuario desactivado",
|
"deactivated_user": "Usuario desactivado",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
"applying": "Aplicando"
|
"applying": "Aplicando",
|
||||||
|
"users": "Usuarios",
|
||||||
|
"admins": "Administradores",
|
||||||
|
"guests": "Invitados",
|
||||||
|
"on_track": "En camino",
|
||||||
|
"off_track": "Fuera de camino",
|
||||||
|
"timeline": "Cronograma",
|
||||||
|
"completion": "Finalización",
|
||||||
|
"upcoming": "Próximo",
|
||||||
|
"completed": "Completado",
|
||||||
|
"in_progress": "En progreso",
|
||||||
|
"planned": "Planificado",
|
||||||
|
"paused": "Pausado"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Eje X",
|
"x_axis": "Eje X",
|
||||||
|
|
@ -1319,19 +1331,6 @@
|
||||||
"custom": "Análisis Personalizado"
|
"custom": "Análisis Personalizado"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Rastrea el progreso, cargas de trabajo y asignaciones. Identifica tendencias, elimina bloqueos y mueve el trabajo más rápido",
|
|
||||||
"description": "Observa el alcance versus la demanda, estimaciones y el aumento del alcance. Obtén el rendimiento por miembros del equipo y equipos, y asegúrate de que tu proyecto se ejecute a tiempo.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Inicia tu primer proyecto",
|
|
||||||
"comic": {
|
|
||||||
"title": "El análisis funciona mejor con Ciclos + Módulos",
|
|
||||||
"description": "Primero, organiza tus elementos de trabajo en Ciclos y, si puedes, agrupa los elementos de trabajo que abarcan más de un ciclo en Módulos. Revisa ambos en la navegación izquierda."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Los elementos de trabajo asignados a ti, desglosados por estado, aparecerán aquí.",
|
"description": "Los elementos de trabajo asignados a ti, desglosados por estado, aparecerán aquí.",
|
||||||
"title": "Aún no hay datos"
|
"title": "Aún no hay datos"
|
||||||
|
|
@ -1347,21 +1346,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Creado vs Resuelto",
|
"created_vs_resolved": "Creado vs Resuelto",
|
||||||
"customized_insights": "Información personalizada",
|
"customized_insights": "Información personalizada",
|
||||||
"backlog_work_items": "Elementos de trabajo en backlog",
|
"backlog_work_items": "{entity} en backlog",
|
||||||
"active_projects": "Proyectos activos",
|
"active_projects": "Proyectos activos",
|
||||||
"trend_on_charts": "Tendencia en gráficos",
|
"trend_on_charts": "Tendencia en gráficos",
|
||||||
"all_projects": "Todos los proyectos",
|
"all_projects": "Todos los proyectos",
|
||||||
"summary_of_projects": "Resumen de proyectos",
|
"summary_of_projects": "Resumen de proyectos",
|
||||||
"project_insights": "Información del proyecto",
|
"project_insights": "Información del proyecto",
|
||||||
"started_work_items": "Elementos de trabajo iniciados",
|
"started_work_items": "{entity} iniciados",
|
||||||
"total_work_items": "Total de elementos de trabajo",
|
"total_work_items": "Total de {entity}",
|
||||||
"total_projects": "Total de proyectos",
|
"total_projects": "Total de proyectos",
|
||||||
"total_admins": "Total de administradores",
|
"total_admins": "Total de administradores",
|
||||||
"total_users": "Total de usuarios",
|
"total_users": "Total de usuarios",
|
||||||
"total_intake": "Ingreso total",
|
"total_intake": "Ingreso total",
|
||||||
"un_started_work_items": "Elementos de trabajo no iniciados",
|
"un_started_work_items": "{entity} no iniciados",
|
||||||
"total_guests": "Total de invitados",
|
"total_guests": "Total de invitados",
|
||||||
"completed_work_items": "Elementos de trabajo completados"
|
"completed_work_items": "{entity} completados",
|
||||||
|
"total": "Total de {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Proyecto} other {Proyectos}}",
|
"label": "{count, plural, one {Proyecto} other {Proyectos}}",
|
||||||
|
|
|
||||||
|
|
@ -867,7 +867,19 @@
|
||||||
"view": "Afficher",
|
"view": "Afficher",
|
||||||
"deactivated_user": "Utilisateur désactivé",
|
"deactivated_user": "Utilisateur désactivé",
|
||||||
"apply": "Appliquer",
|
"apply": "Appliquer",
|
||||||
"applying": "Application"
|
"applying": "Application",
|
||||||
|
"users": "Utilisateurs",
|
||||||
|
"admins": "Administrateurs",
|
||||||
|
"guests": "Invités",
|
||||||
|
"on_track": "Sur la bonne voie",
|
||||||
|
"off_track": "Hors de la bonne voie",
|
||||||
|
"timeline": "Chronologie",
|
||||||
|
"completion": "Achèvement",
|
||||||
|
"upcoming": "À venir",
|
||||||
|
"completed": "Terminé",
|
||||||
|
"in_progress": "En cours",
|
||||||
|
"planned": "Planifié",
|
||||||
|
"paused": "En pause"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Axe X",
|
"x_axis": "Axe X",
|
||||||
|
|
@ -1317,19 +1329,6 @@
|
||||||
"custom": "Analytique Personnalisée"
|
"custom": "Analytique Personnalisée"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Suivez les progrès, les charges de travail et les allocations. Repérez les tendances, supprimez les blocages et accélérez le travail",
|
|
||||||
"description": "Visualisez la portée par rapport à la demande, les estimations et l'augmentation de la portée. Obtenez les performances par membres de l'équipe et équipes, et assurez-vous que votre projet se déroule dans les délais.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Commencez votre premier projet",
|
|
||||||
"comic": {
|
|
||||||
"title": "L'analytique fonctionne mieux avec les Cycles + Modules",
|
|
||||||
"description": "D'abord, planifiez vos éléments de travail dans des Cycles et, si possible, regroupez les éléments de travail qui s'étendent sur plus d'un cycle dans des Modules. Consultez les deux dans la navigation de gauche."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Les éléments de travail qui vous sont assignés, répartis par état, s'afficheront ici.",
|
"description": "Les éléments de travail qui vous sont assignés, répartis par état, s'afficheront ici.",
|
||||||
"title": "Pas encore de données"
|
"title": "Pas encore de données"
|
||||||
|
|
@ -1345,21 +1344,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Créé vs Résolu",
|
"created_vs_resolved": "Créé vs Résolu",
|
||||||
"customized_insights": "Informations personnalisées",
|
"customized_insights": "Informations personnalisées",
|
||||||
"backlog_work_items": "Éléments de travail en backlog",
|
"backlog_work_items": "{entity} en backlog",
|
||||||
"active_projects": "Projets actifs",
|
"active_projects": "Projets actifs",
|
||||||
"trend_on_charts": "Tendance sur les graphiques",
|
"trend_on_charts": "Tendance sur les graphiques",
|
||||||
"all_projects": "Tous les projets",
|
"all_projects": "Tous les projets",
|
||||||
"summary_of_projects": "Résumé des projets",
|
"summary_of_projects": "Résumé des projets",
|
||||||
"project_insights": "Aperçus du projet",
|
"project_insights": "Aperçus du projet",
|
||||||
"started_work_items": "Éléments de travail commencés",
|
"started_work_items": "{entity} commencés",
|
||||||
"total_work_items": "Total des éléments de travail",
|
"total_work_items": "Total des {entity}",
|
||||||
"total_projects": "Total des projets",
|
"total_projects": "Total des projets",
|
||||||
"total_admins": "Total des administrateurs",
|
"total_admins": "Total des administrateurs",
|
||||||
"total_users": "Nombre total d'utilisateurs",
|
"total_users": "Nombre total d'utilisateurs",
|
||||||
"total_intake": "Revenu total",
|
"total_intake": "Revenu total",
|
||||||
"un_started_work_items": "Éléments de travail non commencés",
|
"un_started_work_items": "{entity} non commencés",
|
||||||
"total_guests": "Nombre total d'invités",
|
"total_guests": "Nombre total d'invités",
|
||||||
"completed_work_items": "Éléments de travail terminés"
|
"completed_work_items": "{entity} terminés",
|
||||||
|
"total": "Total des {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Projet} other {Projets}}",
|
"label": "{count, plural, one {Projet} other {Projets}}",
|
||||||
|
|
|
||||||
|
|
@ -866,7 +866,19 @@
|
||||||
"view": "Lihat",
|
"view": "Lihat",
|
||||||
"deactivated_user": "Pengguna dinonaktifkan",
|
"deactivated_user": "Pengguna dinonaktifkan",
|
||||||
"apply": "Terapkan",
|
"apply": "Terapkan",
|
||||||
"applying": "Terapkan"
|
"applying": "Terapkan",
|
||||||
|
"users": "Pengguna",
|
||||||
|
"admins": "Admin",
|
||||||
|
"guests": "Tamu",
|
||||||
|
"on_track": "Sesuai Jalur",
|
||||||
|
"off_track": "Menyimpang",
|
||||||
|
"timeline": "Linimasa",
|
||||||
|
"completion": "Penyelesaian",
|
||||||
|
"upcoming": "Mendatang",
|
||||||
|
"completed": "Selesai",
|
||||||
|
"in_progress": "Sedang berlangsung",
|
||||||
|
"planned": "Direncanakan",
|
||||||
|
"paused": "Dijedaikan"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Sumbu-X",
|
"x_axis": "Sumbu-X",
|
||||||
|
|
@ -1316,19 +1328,6 @@
|
||||||
"custom": "Analitik Kustom"
|
"custom": "Analitik Kustom"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Lacak kemajuan, beban kerja, dan alokasi. Temukan tren, hilangkan penghalang, dan percepat pekerjaan",
|
|
||||||
"description": "Lihat lingkup dibandingkan permintaan, perkiraan, dan lingkup cree. Dapatkan kinerja oleh anggota tim dan tim, dan pastikan proyek Anda berjalan tepat waktu.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Mulai proyek pertama Anda",
|
|
||||||
"comic": {
|
|
||||||
"title": "Analitik bekerja terbaik dengan Siklus + Modul",
|
|
||||||
"description": "Pertama, bagi item kerja Anda ke dalam Siklus dan, jika memungkinkan, kelompokkan item kerja yang menjangkau lebih dari satu siklus ke dalam Modul. Lihat kedua fungsi pada navigasi kiri."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Item pekerjaan yang ditugaskan kepada Anda, dipecah berdasarkan status, akan muncul di sini.",
|
"description": "Item pekerjaan yang ditugaskan kepada Anda, dipecah berdasarkan status, akan muncul di sini.",
|
||||||
"title": "Belum ada data"
|
"title": "Belum ada data"
|
||||||
|
|
@ -1344,21 +1343,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Dibuat vs Diselesaikan",
|
"created_vs_resolved": "Dibuat vs Diselesaikan",
|
||||||
"customized_insights": "Wawasan yang Disesuaikan",
|
"customized_insights": "Wawasan yang Disesuaikan",
|
||||||
"backlog_work_items": "Item pekerjaan backlog",
|
"backlog_work_items": "{entity} backlog",
|
||||||
"active_projects": "Proyek Aktif",
|
"active_projects": "Proyek Aktif",
|
||||||
"trend_on_charts": "Tren pada grafik",
|
"trend_on_charts": "Tren pada grafik",
|
||||||
"all_projects": "Semua Proyek",
|
"all_projects": "Semua Proyek",
|
||||||
"summary_of_projects": "Ringkasan Proyek",
|
"summary_of_projects": "Ringkasan Proyek",
|
||||||
"project_insights": "Wawasan Proyek",
|
"project_insights": "Wawasan Proyek",
|
||||||
"started_work_items": "Item pekerjaan yang telah dimulai",
|
"started_work_items": "{entity} yang telah dimulai",
|
||||||
"total_work_items": "Total item pekerjaan",
|
"total_work_items": "Total {entity}",
|
||||||
"total_projects": "Total Proyek",
|
"total_projects": "Total Proyek",
|
||||||
"total_admins": "Total Admin",
|
"total_admins": "Total Admin",
|
||||||
"total_users": "Total Pengguna",
|
"total_users": "Total Pengguna",
|
||||||
"total_intake": "Total Pemasukan",
|
"total_intake": "Total Pemasukan",
|
||||||
"un_started_work_items": "Item pekerjaan yang belum dimulai",
|
"un_started_work_items": "{entity} yang belum dimulai",
|
||||||
"total_guests": "Total Tamu",
|
"total_guests": "Total Tamu",
|
||||||
"completed_work_items": "Item pekerjaan yang telah selesai"
|
"completed_work_items": "{entity} yang telah selesai",
|
||||||
|
"total": "Total {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Proyek} other {Proyek}}",
|
"label": "{count, plural, one {Proyek} other {Proyek}}",
|
||||||
|
|
|
||||||
|
|
@ -865,7 +865,19 @@
|
||||||
"view": "Visualizza",
|
"view": "Visualizza",
|
||||||
"deactivated_user": "Utente disattivato",
|
"deactivated_user": "Utente disattivato",
|
||||||
"apply": "Applica",
|
"apply": "Applica",
|
||||||
"applying": "Applicazione"
|
"applying": "Applicazione",
|
||||||
|
"users": "Utenti",
|
||||||
|
"admins": "Amministratori",
|
||||||
|
"guests": "Ospiti",
|
||||||
|
"on_track": "In linea",
|
||||||
|
"off_track": "Fuori rotta",
|
||||||
|
"timeline": "Cronologia",
|
||||||
|
"completion": "Completamento",
|
||||||
|
"upcoming": "In arrivo",
|
||||||
|
"completed": "Completato",
|
||||||
|
"in_progress": "In corso",
|
||||||
|
"planned": "Pianificato",
|
||||||
|
"paused": "In pausa"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Asse X",
|
"x_axis": "Asse X",
|
||||||
|
|
@ -1315,19 +1327,6 @@
|
||||||
"custom": "Analisi personalizzata"
|
"custom": "Analisi personalizzata"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Traccia il progresso, i carichi di lavoro e le assegnazioni. Individua tendenze, rimuovi gli ostacoli e accelera il lavoro",
|
|
||||||
"description": "Visualizza l'ambito rispetto alla domanda, le stime e il fenomeno del scope creep. Ottieni le prestazioni dei membri del team e dei team, e assicurati che il tuo progetto rispetti le scadenze.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Inizia il tuo primo progetto",
|
|
||||||
"comic": {
|
|
||||||
"title": "Le analisi funzionano meglio con Cicli + Moduli",
|
|
||||||
"description": "Prima, definisci i tuoi elementi di lavoro in cicli e, se puoi, raggruppa quelli che si estendono per più di un ciclo in moduli. Dai un'occhiata ad entrambi nel menu di sinistra."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Gli elementi di lavoro assegnati a te, suddivisi per stato, verranno visualizzati qui.",
|
"description": "Gli elementi di lavoro assegnati a te, suddivisi per stato, verranno visualizzati qui.",
|
||||||
"title": "Nessun dato disponibile"
|
"title": "Nessun dato disponibile"
|
||||||
|
|
@ -1343,21 +1342,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Creato vs Risolto",
|
"created_vs_resolved": "Creato vs Risolto",
|
||||||
"customized_insights": "Approfondimenti personalizzati",
|
"customized_insights": "Approfondimenti personalizzati",
|
||||||
"backlog_work_items": "Elementi di lavoro nel backlog",
|
"backlog_work_items": "{entity} nel backlog",
|
||||||
"active_projects": "Progetti attivi",
|
"active_projects": "Progetti attivi",
|
||||||
"trend_on_charts": "Tendenza nei grafici",
|
"trend_on_charts": "Tendenza nei grafici",
|
||||||
"all_projects": "Tutti i progetti",
|
"all_projects": "Tutti i progetti",
|
||||||
"summary_of_projects": "Riepilogo dei progetti",
|
"summary_of_projects": "Riepilogo dei progetti",
|
||||||
"project_insights": "Approfondimenti sul progetto",
|
"project_insights": "Approfondimenti sul progetto",
|
||||||
"started_work_items": "Elementi di lavoro iniziati",
|
"started_work_items": "{entity} iniziati",
|
||||||
"total_work_items": "Totale elementi di lavoro",
|
"total_work_items": "Totale {entity}",
|
||||||
"total_projects": "Progetti totali",
|
"total_projects": "Progetti totali",
|
||||||
"total_admins": "Totale amministratori",
|
"total_admins": "Totale amministratori",
|
||||||
"total_users": "Totale utenti",
|
"total_users": "Totale utenti",
|
||||||
"total_intake": "Entrate totali",
|
"total_intake": "Entrate totali",
|
||||||
"un_started_work_items": "Elementi di lavoro non avviati",
|
"un_started_work_items": "{entity} non avviati",
|
||||||
"total_guests": "Totale ospiti",
|
"total_guests": "Totale ospiti",
|
||||||
"completed_work_items": "Elementi di lavoro completati"
|
"completed_work_items": "{entity} completati",
|
||||||
|
"total": "Totale {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Progetto} other {Progetti}}",
|
"label": "{count, plural, one {Progetto} other {Progetti}}",
|
||||||
|
|
|
||||||
|
|
@ -867,7 +867,19 @@
|
||||||
"view": "ビュー",
|
"view": "ビュー",
|
||||||
"deactivated_user": "無効化されたユーザー",
|
"deactivated_user": "無効化されたユーザー",
|
||||||
"apply": "適用",
|
"apply": "適用",
|
||||||
"applying": "適用中"
|
"applying": "適用中",
|
||||||
|
"users": "ユーザー",
|
||||||
|
"admins": "管理者",
|
||||||
|
"guests": "ゲスト",
|
||||||
|
"on_track": "順調",
|
||||||
|
"off_track": "遅れ",
|
||||||
|
"timeline": "タイムライン",
|
||||||
|
"completion": "完了",
|
||||||
|
"upcoming": "今後の予定",
|
||||||
|
"completed": "完了",
|
||||||
|
"in_progress": "進行中",
|
||||||
|
"planned": "計画済み",
|
||||||
|
"paused": "一時停止"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "エックス アクシス",
|
"x_axis": "エックス アクシス",
|
||||||
|
|
@ -1317,19 +1329,6 @@
|
||||||
"custom": "カスタムアナリティクス"
|
"custom": "カスタムアナリティクス"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "進捗、ワークロード、割り当てを追跡。傾向を把握し、ブロッカーを解消して、作業をより速く進めましょう",
|
|
||||||
"description": "スコープと需要、見積もり、スコープクリープを確認できます。チームメンバーとチームのパフォーマンスを把握し、プロジェクトが予定通りに進むようにします。",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "最初のプロジェクトを開始",
|
|
||||||
"comic": {
|
|
||||||
"title": "アナリティクスはサイクル + モジュールで最も効果を発揮",
|
|
||||||
"description": "まず、作業項目をサイクルでタイムボックス化し、可能であれば、複数のサイクルにまたがる作業項目をモジュールにグループ化します。左のナビゲーションで両方を確認してください。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "あなたに割り当てられた作業項目は、ステータスごとに分類されてここに表示されます。",
|
"description": "あなたに割り当てられた作業項目は、ステータスごとに分類されてここに表示されます。",
|
||||||
"title": "まだデータがありません"
|
"title": "まだデータがありません"
|
||||||
|
|
@ -1345,21 +1344,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "作成 vs 解決",
|
"created_vs_resolved": "作成 vs 解決",
|
||||||
"customized_insights": "カスタマイズされたインサイト",
|
"customized_insights": "カスタマイズされたインサイト",
|
||||||
"backlog_work_items": "バックログの作業項目",
|
"backlog_work_items": "バックログの{entity}",
|
||||||
"active_projects": "アクティブなプロジェクト",
|
"active_projects": "アクティブなプロジェクト",
|
||||||
"trend_on_charts": "グラフの傾向",
|
"trend_on_charts": "グラフの傾向",
|
||||||
"all_projects": "すべてのプロジェクト",
|
"all_projects": "すべてのプロジェクト",
|
||||||
"summary_of_projects": "プロジェクトの概要",
|
"summary_of_projects": "プロジェクトの概要",
|
||||||
"project_insights": "プロジェクトのインサイト",
|
"project_insights": "プロジェクトのインサイト",
|
||||||
"started_work_items": "開始された作業項目",
|
"started_work_items": "開始された{entity}",
|
||||||
"total_work_items": "作業項目の合計",
|
"total_work_items": "{entity}の合計",
|
||||||
"total_projects": "プロジェクト合計",
|
"total_projects": "プロジェクト合計",
|
||||||
"total_admins": "管理者の合計",
|
"total_admins": "管理者の合計",
|
||||||
"total_users": "ユーザー総数",
|
"total_users": "ユーザー総数",
|
||||||
"total_intake": "総収入",
|
"total_intake": "総収入",
|
||||||
"un_started_work_items": "未開始の作業項目",
|
"un_started_work_items": "未開始の{entity}",
|
||||||
"total_guests": "ゲストの合計",
|
"total_guests": "ゲストの合計",
|
||||||
"completed_work_items": "完了した作業項目"
|
"completed_work_items": "完了した{entity}",
|
||||||
|
"total": "{entity}の合計"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {プロジェクト} other {プロジェクト}}",
|
"label": "{count, plural, one {プロジェクト} other {プロジェクト}}",
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,19 @@
|
||||||
"view": "보기",
|
"view": "보기",
|
||||||
"deactivated_user": "비활성화된 사용자",
|
"deactivated_user": "비활성화된 사용자",
|
||||||
"apply": "적용",
|
"apply": "적용",
|
||||||
"applying": "적용 중"
|
"applying": "적용 중",
|
||||||
|
"users": "사용자",
|
||||||
|
"admins": "관리자",
|
||||||
|
"guests": "게스트",
|
||||||
|
"on_track": "계획대로 진행 중",
|
||||||
|
"off_track": "계획 이탈",
|
||||||
|
"timeline": "타임라인",
|
||||||
|
"completion": "완료",
|
||||||
|
"upcoming": "예정된",
|
||||||
|
"completed": "완료됨",
|
||||||
|
"in_progress": "진행 중",
|
||||||
|
"planned": "계획된",
|
||||||
|
"paused": "일시 중지됨"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "X축",
|
"x_axis": "X축",
|
||||||
|
|
@ -1318,19 +1330,6 @@
|
||||||
"custom": "맞춤형 분석"
|
"custom": "맞춤형 분석"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "진행 상황, 작업량 및 할당을 추적하세요. 트렌드를 파악하고, 차단 요소를 제거하며, 작업을 더 빠르게 진행하세요",
|
|
||||||
"description": "범위 대 수요, 추정치 및 범위 크리프를 확인하세요. 팀원과 팀의 성과를 확인하고 프로젝트가 제시간에 진행되도록 하세요.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "첫 번째 프로젝트 시작",
|
|
||||||
"comic": {
|
|
||||||
"title": "분석은 주기 + 모듈과 함께 작동합니다",
|
|
||||||
"description": "먼저 작업 항목을 주기로 시간 상자화하고, 주기를 초과하는 작업 항목을 모듈로 그룹화하세요. 왼쪽 탐색에서 둘 다 확인하세요."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "귀하에게 할당된 작업 항목이 상태별로 나누어 여기에 표시됩니다.",
|
"description": "귀하에게 할당된 작업 항목이 상태별로 나누어 여기에 표시됩니다.",
|
||||||
"title": "아직 데이터가 없습니다"
|
"title": "아직 데이터가 없습니다"
|
||||||
|
|
@ -1346,21 +1345,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "생성됨 vs 해결됨",
|
"created_vs_resolved": "생성됨 vs 해결됨",
|
||||||
"customized_insights": "맞춤형 인사이트",
|
"customized_insights": "맞춤형 인사이트",
|
||||||
"backlog_work_items": "백로그 작업 항목",
|
"backlog_work_items": "백로그 {entity}",
|
||||||
"active_projects": "활성 프로젝트",
|
"active_projects": "활성 프로젝트",
|
||||||
"trend_on_charts": "차트의 추세",
|
"trend_on_charts": "차트의 추세",
|
||||||
"all_projects": "모든 프로젝트",
|
"all_projects": "모든 프로젝트",
|
||||||
"summary_of_projects": "프로젝트 요약",
|
"summary_of_projects": "프로젝트 요약",
|
||||||
"project_insights": "프로젝트 인사이트",
|
"project_insights": "프로젝트 인사이트",
|
||||||
"started_work_items": "시작된 작업 항목",
|
"started_work_items": "시작된 {entity}",
|
||||||
"total_work_items": "총 작업 항목",
|
"total_work_items": "총 {entity}",
|
||||||
"total_projects": "총 프로젝트 수",
|
"total_projects": "총 프로젝트 수",
|
||||||
"total_admins": "총 관리자 수",
|
"total_admins": "총 관리자 수",
|
||||||
"total_users": "총 사용자 수",
|
"total_users": "총 사용자 수",
|
||||||
"total_intake": "총 수입",
|
"total_intake": "총 수입",
|
||||||
"un_started_work_items": "시작되지 않은 작업 항목",
|
"un_started_work_items": "시작되지 않은 {entity}",
|
||||||
"total_guests": "총 게스트 수",
|
"total_guests": "총 게스트 수",
|
||||||
"completed_work_items": "완료된 작업 항목"
|
"completed_work_items": "완료된 {entity}",
|
||||||
|
"total": "총 {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {프로젝트} other {프로젝트}}",
|
"label": "{count, plural, one {프로젝트} other {프로젝트}}",
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,19 @@
|
||||||
"view": "Widok",
|
"view": "Widok",
|
||||||
"deactivated_user": "Dezaktywowany użytkownik",
|
"deactivated_user": "Dezaktywowany użytkownik",
|
||||||
"apply": "Zastosuj",
|
"apply": "Zastosuj",
|
||||||
"applying": "Zastosowanie"
|
"applying": "Zastosowanie",
|
||||||
|
"users": "Użytkownicy",
|
||||||
|
"admins": "Administratorzy",
|
||||||
|
"guests": "Goście",
|
||||||
|
"on_track": "Na dobrej drodze",
|
||||||
|
"off_track": "Poza planem",
|
||||||
|
"timeline": "Oś czasu",
|
||||||
|
"completion": "Zakończenie",
|
||||||
|
"upcoming": "Nadchodzące",
|
||||||
|
"completed": "Zakończone",
|
||||||
|
"in_progress": "W trakcie",
|
||||||
|
"planned": "Zaplanowane",
|
||||||
|
"paused": "Wstrzymane"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Oś X",
|
"x_axis": "Oś X",
|
||||||
|
|
@ -1318,19 +1330,6 @@
|
||||||
"custom": "Analizy niestandardowe"
|
"custom": "Analizy niestandardowe"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Śledź postępy, obciążenie i alokacje. Identyfikuj trendy, usuwaj przeszkody i przyspieszaj pracę",
|
|
||||||
"description": "Obserwuj zakres vs. zapotrzebowanie, szacunki i zakres. Sprawdzaj wydajność członków i zespołów, upewnij się, że projekty kończą się na czas.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Zacznij pierwszy projekt",
|
|
||||||
"comic": {
|
|
||||||
"title": "Analizy najlepiej działają z Cyklem + Modułami",
|
|
||||||
"description": "Najpierw ogranicz pracę w cyklach i grupuj zadania w modułach obejmujących wiele cykli. Znajdziesz je w menu po lewej."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Przypisane do Ciebie elementy pracy, podzielone według stanu, pojawią się tutaj.",
|
"description": "Przypisane do Ciebie elementy pracy, podzielone według stanu, pojawią się tutaj.",
|
||||||
"title": "Brak danych"
|
"title": "Brak danych"
|
||||||
|
|
@ -1346,21 +1345,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Utworzone vs Rozwiązane",
|
"created_vs_resolved": "Utworzone vs Rozwiązane",
|
||||||
"customized_insights": "Dostosowane informacje",
|
"customized_insights": "Dostosowane informacje",
|
||||||
"backlog_work_items": "Elementy pracy w backlogu",
|
"backlog_work_items": "{entity} w backlogu",
|
||||||
"active_projects": "Aktywne projekty",
|
"active_projects": "Aktywne projekty",
|
||||||
"trend_on_charts": "Trend na wykresach",
|
"trend_on_charts": "Trend na wykresach",
|
||||||
"all_projects": "Wszystkie projekty",
|
"all_projects": "Wszystkie projekty",
|
||||||
"summary_of_projects": "Podsumowanie projektów",
|
"summary_of_projects": "Podsumowanie projektów",
|
||||||
"project_insights": "Wgląd w projekt",
|
"project_insights": "Wgląd w projekt",
|
||||||
"started_work_items": "Rozpoczęte elementy pracy",
|
"started_work_items": "Rozpoczęte {entity}",
|
||||||
"total_work_items": "Łączna liczba elementów pracy",
|
"total_work_items": "Łączna liczba {entity}",
|
||||||
"total_projects": "Łączna liczba projektów",
|
"total_projects": "Łączna liczba projektów",
|
||||||
"total_admins": "Łączna liczba administratorów",
|
"total_admins": "Łączna liczba administratorów",
|
||||||
"total_users": "Łączna liczba użytkowników",
|
"total_users": "Łączna liczba użytkowników",
|
||||||
"total_intake": "Całkowity dochód",
|
"total_intake": "Całkowity dochód",
|
||||||
"un_started_work_items": "Nierozpoczęte elementy pracy",
|
"un_started_work_items": "Nierozpoczęte {entity}",
|
||||||
"total_guests": "Łączna liczba gości",
|
"total_guests": "Łączna liczba gości",
|
||||||
"completed_work_items": "Ukończone elementy pracy"
|
"completed_work_items": "Ukończone {entity}",
|
||||||
|
"total": "Łączna liczba {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektów}}",
|
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektów}}",
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,19 @@
|
||||||
"view": "Visualizar",
|
"view": "Visualizar",
|
||||||
"deactivated_user": "Usuário desativado",
|
"deactivated_user": "Usuário desativado",
|
||||||
"apply": "Aplicar",
|
"apply": "Aplicar",
|
||||||
"applying": "Aplicando"
|
"applying": "Aplicando",
|
||||||
|
"users": "Usuários",
|
||||||
|
"admins": "Administradores",
|
||||||
|
"guests": "Convidados",
|
||||||
|
"on_track": "No caminho certo",
|
||||||
|
"off_track": "Fora do caminho",
|
||||||
|
"timeline": "Linha do tempo",
|
||||||
|
"completion": "Conclusão",
|
||||||
|
"upcoming": "Próximo",
|
||||||
|
"completed": "Concluído",
|
||||||
|
"in_progress": "Em andamento",
|
||||||
|
"planned": "Planejado",
|
||||||
|
"paused": "Pausado"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Eixo X",
|
"x_axis": "Eixo X",
|
||||||
|
|
@ -1318,19 +1330,6 @@
|
||||||
"custom": "Análises Personalizadas"
|
"custom": "Análises Personalizadas"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Acompanhe o progresso, as cargas de trabalho e as alocações. Identifique tendências, remova bloqueadores e mova o trabalho mais rapidamente",
|
|
||||||
"description": "Veja o escopo versus a demanda, as estimativas e o aumento do escopo. Obtenha o desempenho por membros da equipe e equipes, e certifique-se de que seu projeto seja executado no prazo.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Comece seu primeiro projeto",
|
|
||||||
"comic": {
|
|
||||||
"title": "A análise funciona melhor com Ciclos + Módulos",
|
|
||||||
"description": "Primeiro, coloque seus itens de trabalho em Ciclos e, se puder, agrupe os itens de trabalho que abrangem mais de um ciclo em Módulos. Confira ambos na navegação à esquerda."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Os itens de trabalho atribuídos a você, divididos por estado, aparecerão aqui.",
|
"description": "Os itens de trabalho atribuídos a você, divididos por estado, aparecerão aqui.",
|
||||||
"title": "Ainda não há dados"
|
"title": "Ainda não há dados"
|
||||||
|
|
@ -1346,21 +1345,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Criado vs Resolvido",
|
"created_vs_resolved": "Criado vs Resolvido",
|
||||||
"customized_insights": "Insights personalizados",
|
"customized_insights": "Insights personalizados",
|
||||||
"backlog_work_items": "Itens de trabalho no backlog",
|
"backlog_work_items": "{entity} no backlog",
|
||||||
"active_projects": "Projetos ativos",
|
"active_projects": "Projetos ativos",
|
||||||
"trend_on_charts": "Tendência nos gráficos",
|
"trend_on_charts": "Tendência nos gráficos",
|
||||||
"all_projects": "Todos os projetos",
|
"all_projects": "Todos os projetos",
|
||||||
"summary_of_projects": "Resumo dos projetos",
|
"summary_of_projects": "Resumo dos projetos",
|
||||||
"project_insights": "Insights do projeto",
|
"project_insights": "Insights do projeto",
|
||||||
"started_work_items": "Itens de trabalho iniciados",
|
"started_work_items": "{entity} iniciados",
|
||||||
"total_work_items": "Total de itens de trabalho",
|
"total_work_items": "Total de {entity}",
|
||||||
"total_projects": "Total de projetos",
|
"total_projects": "Total de projetos",
|
||||||
"total_admins": "Total de administradores",
|
"total_admins": "Total de administradores",
|
||||||
"total_users": "Total de usuários",
|
"total_users": "Total de usuários",
|
||||||
"total_intake": "Receita total",
|
"total_intake": "Receita total",
|
||||||
"un_started_work_items": "Itens de trabalho não iniciados",
|
"un_started_work_items": "{entity} não iniciados",
|
||||||
"total_guests": "Total de convidados",
|
"total_guests": "Total de convidados",
|
||||||
"completed_work_items": "Itens de trabalho concluídos"
|
"completed_work_items": "{entity} concluídos",
|
||||||
|
"total": "Total de {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Projeto} other {Projetos}}",
|
"label": "{count, plural, one {Projeto} other {Projetos}}",
|
||||||
|
|
|
||||||
|
|
@ -866,7 +866,19 @@
|
||||||
"view": "Vizualizează",
|
"view": "Vizualizează",
|
||||||
"deactivated_user": "Utilizator dezactivat",
|
"deactivated_user": "Utilizator dezactivat",
|
||||||
"apply": "Aplică",
|
"apply": "Aplică",
|
||||||
"applying": "Aplicând"
|
"applying": "Aplicând",
|
||||||
|
"users": "Utilizatori",
|
||||||
|
"admins": "Administratori",
|
||||||
|
"guests": "Invitați",
|
||||||
|
"on_track": "Pe drumul cel bun",
|
||||||
|
"off_track": "În afara traiectoriei",
|
||||||
|
"timeline": "Cronologie",
|
||||||
|
"completion": "Finalizare",
|
||||||
|
"upcoming": "Viitor",
|
||||||
|
"completed": "Finalizat",
|
||||||
|
"in_progress": "În desfășurare",
|
||||||
|
"planned": "Planificat",
|
||||||
|
"paused": "Pauzat"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "axa-X",
|
"x_axis": "axa-X",
|
||||||
|
|
@ -1316,19 +1328,6 @@
|
||||||
"custom": "Analitice personalizate"
|
"custom": "Analitice personalizate"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Urmărește progresul, activitățile și alocările. Observă tendințele, elimină blocajele și accelerează munca",
|
|
||||||
"description": "Vezi raportul dintre activitățile asumate și cerere, estimările și eventualele extinderi neplanificate ale activităților asumate. Obține performanța pe membri și echipe și asigură-te că proiectul tău se încadrează în timp.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Începe primul tău proiect",
|
|
||||||
"comic": {
|
|
||||||
"title": "Statisticile funcționează cel mai bine cu Cicluri + Module",
|
|
||||||
"description": "Mai întâi, încadrează-ți activitățile în Cicluri și, dacă poți, grupează-le pe cele care se întind pe mai multe cicluri în Module. Le găsești în meniul din stânga."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Elementele de lucru atribuite ție, împărțite pe stări, vor apărea aici.",
|
"description": "Elementele de lucru atribuite ție, împărțite pe stări, vor apărea aici.",
|
||||||
"title": "Nu există date încă"
|
"title": "Nu există date încă"
|
||||||
|
|
@ -1344,21 +1343,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Creat vs Rezolvat",
|
"created_vs_resolved": "Creat vs Rezolvat",
|
||||||
"customized_insights": "Perspective personalizate",
|
"customized_insights": "Perspective personalizate",
|
||||||
"backlog_work_items": "Elemente de lucru din backlog",
|
"backlog_work_items": "{entity} din backlog",
|
||||||
"active_projects": "Proiecte active",
|
"active_projects": "Proiecte active",
|
||||||
"trend_on_charts": "Tendință în grafice",
|
"trend_on_charts": "Tendință în grafice",
|
||||||
"all_projects": "Toate proiectele",
|
"all_projects": "Toate proiectele",
|
||||||
"summary_of_projects": "Sumarul proiectelor",
|
"summary_of_projects": "Sumarul proiectelor",
|
||||||
"project_insights": "Informații despre proiect",
|
"project_insights": "Informații despre proiect",
|
||||||
"started_work_items": "Elemente de lucru începute",
|
"started_work_items": "{entity} începute",
|
||||||
"total_work_items": "Totalul elementelor de lucru",
|
"total_work_items": "Totalul {entity}",
|
||||||
"total_projects": "Total proiecte",
|
"total_projects": "Total proiecte",
|
||||||
"total_admins": "Total administratori",
|
"total_admins": "Total administratori",
|
||||||
"total_users": "Total utilizatori",
|
"total_users": "Total utilizatori",
|
||||||
"total_intake": "Venit total",
|
"total_intake": "Venit total",
|
||||||
"un_started_work_items": "Elemente de lucru neîncepute",
|
"un_started_work_items": "{entity} neîncepute",
|
||||||
"total_guests": "Total invitați",
|
"total_guests": "Total invitați",
|
||||||
"completed_work_items": "Elemente de lucru finalizate"
|
"completed_work_items": "{entity} finalizate",
|
||||||
|
"total": "Totalul {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Proiect} other {Proiecte}}",
|
"label": "{count, plural, one {Proiect} other {Proiecte}}",
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,19 @@
|
||||||
"view": "Просмотр",
|
"view": "Просмотр",
|
||||||
"deactivated_user": "Деактивированный пользователь",
|
"deactivated_user": "Деактивированный пользователь",
|
||||||
"apply": "Применить",
|
"apply": "Применить",
|
||||||
"applying": "Применение"
|
"applying": "Применение",
|
||||||
|
"users": "Пользователи",
|
||||||
|
"admins": "Администраторы",
|
||||||
|
"guests": "Гости",
|
||||||
|
"on_track": "По плану",
|
||||||
|
"off_track": "Отклонение от плана",
|
||||||
|
"timeline": "Хронология",
|
||||||
|
"completion": "Завершение",
|
||||||
|
"upcoming": "Предстоящие",
|
||||||
|
"completed": "Завершено",
|
||||||
|
"in_progress": "В процессе",
|
||||||
|
"planned": "Запланировано",
|
||||||
|
"paused": "На паузе"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Ось X",
|
"x_axis": "Ось X",
|
||||||
|
|
@ -1318,19 +1330,6 @@
|
||||||
"custom": "Пользовательская аналитика"
|
"custom": "Пользовательская аналитика"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Отслеживайте прогресс, загрузку и распределение ресурсов",
|
|
||||||
"description": "Анализируйте объёмы работ, оценивайте сроки и контролируйте выполнение проектов. Отслеживайте производительность команды и соблюдайте сроки.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Начать первый проект",
|
|
||||||
"comic": {
|
|
||||||
"title": "Аналитика лучше всего работает с Циклами + Модулями",
|
|
||||||
"description": "Сначала группируйте рабочие элементы в Циклы, а при возможности - объединяйте рабочие элементы в Модули. Найдите оба раздела в левом меню."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Назначенные вам рабочие элементы, разбитые по статусам, появятся здесь.",
|
"description": "Назначенные вам рабочие элементы, разбитые по статусам, появятся здесь.",
|
||||||
"title": "Данных пока нет"
|
"title": "Данных пока нет"
|
||||||
|
|
@ -1346,21 +1345,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Создано vs Решено",
|
"created_vs_resolved": "Создано vs Решено",
|
||||||
"customized_insights": "Индивидуальные аналитические данные",
|
"customized_insights": "Индивидуальные аналитические данные",
|
||||||
"backlog_work_items": "Элементы работы в бэклоге",
|
"backlog_work_items": "{entity} в бэклоге",
|
||||||
"active_projects": "Активные проекты",
|
"active_projects": "Активные проекты",
|
||||||
"trend_on_charts": "Тренд на графиках",
|
"trend_on_charts": "Тренд на графиках",
|
||||||
"all_projects": "Все проекты",
|
"all_projects": "Все проекты",
|
||||||
"summary_of_projects": "Сводка по проектам",
|
"summary_of_projects": "Сводка по проектам",
|
||||||
"project_insights": "Аналитика проекта",
|
"project_insights": "Аналитика проекта",
|
||||||
"started_work_items": "Начатые рабочие элементы",
|
"started_work_items": "Начатые {entity}",
|
||||||
"total_work_items": "Общее количество рабочих элементов",
|
"total_work_items": "Общее количество {entity}",
|
||||||
"total_projects": "Всего проектов",
|
"total_projects": "Всего проектов",
|
||||||
"total_admins": "Всего администраторов",
|
"total_admins": "Всего администраторов",
|
||||||
"total_users": "Всего пользователей",
|
"total_users": "Всего пользователей",
|
||||||
"total_intake": "Общий доход",
|
"total_intake": "Общий доход",
|
||||||
"un_started_work_items": "Не начатые рабочие элементы",
|
"un_started_work_items": "Не начатые {entity}",
|
||||||
"total_guests": "Всего гостей",
|
"total_guests": "Всего гостей",
|
||||||
"completed_work_items": "Завершённые рабочие элементы"
|
"completed_work_items": "Завершённые {entity}",
|
||||||
|
"total": "Общее количество {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Проект} other {Проекты}}",
|
"label": "{count, plural, one {Проект} other {Проекты}}",
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,19 @@
|
||||||
"view": "Zobraziť",
|
"view": "Zobraziť",
|
||||||
"deactivated_user": "Deaktivovaný používateľ",
|
"deactivated_user": "Deaktivovaný používateľ",
|
||||||
"apply": "Použiť",
|
"apply": "Použiť",
|
||||||
"applying": "Používanie"
|
"applying": "Používanie",
|
||||||
|
"users": "Používatelia",
|
||||||
|
"admins": "Administrátori",
|
||||||
|
"guests": "Hostia",
|
||||||
|
"on_track": "Na správnej ceste",
|
||||||
|
"off_track": "Mimo plán",
|
||||||
|
"timeline": "Časová os",
|
||||||
|
"completion": "Dokončenie",
|
||||||
|
"upcoming": "Nadchádzajúce",
|
||||||
|
"completed": "Dokončené",
|
||||||
|
"in_progress": "Prebieha",
|
||||||
|
"planned": "Plánované",
|
||||||
|
"paused": "Pozastavené"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Os X",
|
"x_axis": "Os X",
|
||||||
|
|
@ -1318,19 +1330,6 @@
|
||||||
"custom": "Vlastná analytika"
|
"custom": "Vlastná analytika"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Sledujte pokrok, vyťaženie a alokácie. Identifikujte trendy, odstráňte prekážky a zrýchlite prácu",
|
|
||||||
"description": "Sledujte rozsah vs. dopyt, odhady a rozsah. Zistite výkonnosť členov a tímov, zabezpečte včasné dokončenie projektov.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Začnite prvý projekt",
|
|
||||||
"comic": {
|
|
||||||
"title": "Analytika funguje najlepšie s Cykly + Moduly",
|
|
||||||
"description": "Najprv časovo ohraničte prácu do cyklov a zoskupte položky presahujúce cyklus do modulov. Nájdete ich v ľavom menu."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Pracovné položky priradené vám, rozdelené podľa stavu, sa zobrazia tu.",
|
"description": "Pracovné položky priradené vám, rozdelené podľa stavu, sa zobrazia tu.",
|
||||||
"title": "Zatiaľ žiadne údaje"
|
"title": "Zatiaľ žiadne údaje"
|
||||||
|
|
@ -1346,21 +1345,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Vytvorené vs Vyriešené",
|
"created_vs_resolved": "Vytvorené vs Vyriešené",
|
||||||
"customized_insights": "Prispôsobené prehľady",
|
"customized_insights": "Prispôsobené prehľady",
|
||||||
"backlog_work_items": "Pracovné položky v backlogu",
|
"backlog_work_items": "{entity} v backlogu",
|
||||||
"active_projects": "Aktívne projekty",
|
"active_projects": "Aktívne projekty",
|
||||||
"trend_on_charts": "Trend na grafoch",
|
"trend_on_charts": "Trend na grafoch",
|
||||||
"all_projects": "Všetky projekty",
|
"all_projects": "Všetky projekty",
|
||||||
"summary_of_projects": "Súhrn projektov",
|
"summary_of_projects": "Súhrn projektov",
|
||||||
"project_insights": "Prehľad projektu",
|
"project_insights": "Prehľad projektu",
|
||||||
"started_work_items": "Spustené pracovné položky",
|
"started_work_items": "Spustené {entity}",
|
||||||
"total_work_items": "Celkový počet pracovných položiek",
|
"total_work_items": "Celkový počet {entity}",
|
||||||
"total_projects": "Celkový počet projektov",
|
"total_projects": "Celkový počet projektov",
|
||||||
"total_admins": "Celkový počet administrátorov",
|
"total_admins": "Celkový počet administrátorov",
|
||||||
"total_users": "Celkový počet používateľov",
|
"total_users": "Celkový počet používateľov",
|
||||||
"total_intake": "Celkový príjem",
|
"total_intake": "Celkový príjem",
|
||||||
"un_started_work_items": "Nespustené pracovné položky",
|
"un_started_work_items": "Nespustené {entity}",
|
||||||
"total_guests": "Celkový počet hostí",
|
"total_guests": "Celkový počet hostí",
|
||||||
"completed_work_items": "Dokončené pracovné položky"
|
"completed_work_items": "Dokončené {entity}",
|
||||||
|
"total": "Celkový počet {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektov}}",
|
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektov}}",
|
||||||
|
|
|
||||||
|
|
@ -869,7 +869,19 @@
|
||||||
"view": "Görünüm",
|
"view": "Görünüm",
|
||||||
"deactivated_user": "Devre dışı bırakılmış kullanıcı",
|
"deactivated_user": "Devre dışı bırakılmış kullanıcı",
|
||||||
"apply": "Uygula",
|
"apply": "Uygula",
|
||||||
"applying": "Uygulanıyor"
|
"applying": "Uygulanıyor",
|
||||||
|
"users": "Kullanıcılar",
|
||||||
|
"admins": "Yöneticiler",
|
||||||
|
"guests": "Misafirler",
|
||||||
|
"on_track": "Yolunda",
|
||||||
|
"off_track": "Yolunda değil",
|
||||||
|
"timeline": "Zaman çizelgesi",
|
||||||
|
"completion": "Tamamlama",
|
||||||
|
"upcoming": "Yaklaşan",
|
||||||
|
"completed": "Tamamlandı",
|
||||||
|
"in_progress": "Devam ediyor",
|
||||||
|
"planned": "Planlandı",
|
||||||
|
"paused": "Durduruldu"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "X ekseni",
|
"x_axis": "X ekseni",
|
||||||
|
|
@ -1319,19 +1331,6 @@
|
||||||
"custom": "Özel Analitik"
|
"custom": "Özel Analitik"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "İlerlemeyi, iş yükünü ve tahsisatları izleyin. Eğilimleri tespit edin, engelleri kaldırın ve işleri hızlandırın",
|
|
||||||
"description": "Kapsam ve talep, tahminler ve kapsam genişlemesini görün. Takım üyeleri ve ekiplerin performansını izleyin ve projenizin zamanında ilerlemesini sağlayın.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "İlk projenizi başlatın",
|
|
||||||
"comic": {
|
|
||||||
"title": "Analitik Döngüler + Modüllerle en iyi şekilde çalışır",
|
|
||||||
"description": "Öncelikle, iş öğelerinizi Döngülere zamanlayın ve mümkünse, bir döngüden uzun süren iş öğelerini Modüllerde gruplayın. Her ikisini de sol gezintide bulabilirsiniz."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Size atanan iş öğeleri, duruma göre ayrılarak burada gösterilecektir.",
|
"description": "Size atanan iş öğeleri, duruma göre ayrılarak burada gösterilecektir.",
|
||||||
"title": "Henüz veri yok"
|
"title": "Henüz veri yok"
|
||||||
|
|
@ -1347,21 +1346,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Oluşturulan vs Çözülen",
|
"created_vs_resolved": "Oluşturulan vs Çözülen",
|
||||||
"customized_insights": "Özelleştirilmiş İçgörüler",
|
"customized_insights": "Özelleştirilmiş İçgörüler",
|
||||||
"backlog_work_items": "Backlog iş öğeleri",
|
"backlog_work_items": "Backlog {entity}",
|
||||||
"active_projects": "Aktif Projeler",
|
"active_projects": "Aktif Projeler",
|
||||||
"trend_on_charts": "Grafiklerdeki eğilim",
|
"trend_on_charts": "Grafiklerdeki eğilim",
|
||||||
"all_projects": "Tüm Projeler",
|
"all_projects": "Tüm Projeler",
|
||||||
"summary_of_projects": "Projelerin Özeti",
|
"summary_of_projects": "Projelerin Özeti",
|
||||||
"project_insights": "Proje İçgörüleri",
|
"project_insights": "Proje İçgörüleri",
|
||||||
"started_work_items": "Başlatılan iş öğeleri",
|
"started_work_items": "Başlatılan {entity}",
|
||||||
"total_work_items": "Toplam iş öğesi",
|
"total_work_items": "Toplam {entity}",
|
||||||
"total_projects": "Toplam Proje",
|
"total_projects": "Toplam Proje",
|
||||||
"total_admins": "Toplam Yönetici",
|
"total_admins": "Toplam Yönetici",
|
||||||
"total_users": "Toplam Kullanıcı",
|
"total_users": "Toplam Kullanıcı",
|
||||||
"total_intake": "Toplam Gelir",
|
"total_intake": "Toplam Gelir",
|
||||||
"un_started_work_items": "Başlanmamış iş öğeleri",
|
"un_started_work_items": "Başlanmamış {entity}",
|
||||||
"total_guests": "Toplam Misafir",
|
"total_guests": "Toplam Misafir",
|
||||||
"completed_work_items": "Tamamlanmış iş öğeleri"
|
"completed_work_items": "Tamamlanmış {entity}",
|
||||||
|
"total": "Toplam {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Proje} other {Projeler}}",
|
"label": "{count, plural, one {Proje} other {Projeler}}",
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,19 @@
|
||||||
"view": "Подання",
|
"view": "Подання",
|
||||||
"deactivated_user": "Деактивований користувач",
|
"deactivated_user": "Деактивований користувач",
|
||||||
"apply": "Застосувати",
|
"apply": "Застосувати",
|
||||||
"applying": "Застосовується"
|
"applying": "Застосовується",
|
||||||
|
"users": "Користувачі",
|
||||||
|
"admins": "Адміністратори",
|
||||||
|
"guests": "Гості",
|
||||||
|
"on_track": "У межах графіку",
|
||||||
|
"off_track": "Поза графіком",
|
||||||
|
"timeline": "Хронологія",
|
||||||
|
"completion": "Завершення",
|
||||||
|
"upcoming": "Майбутнє",
|
||||||
|
"completed": "Завершено",
|
||||||
|
"in_progress": "В процесі",
|
||||||
|
"planned": "Заплановано",
|
||||||
|
"paused": "Призупинено"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Вісь X",
|
"x_axis": "Вісь X",
|
||||||
|
|
@ -1318,19 +1330,6 @@
|
||||||
"custom": "Користувацька аналітика"
|
"custom": "Користувацька аналітика"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Відстежуйте прогрес, навантаження й розподіл. Виявляйте тенденції, усувайте перешкоди й прискорюйте роботу",
|
|
||||||
"description": "Стежте за обсягом проти попиту, оцінками та обсягом. Визначайте ефективність учасників і команд, аби вчасно виконувати проєкти.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Розпочніть перший проєкт",
|
|
||||||
"comic": {
|
|
||||||
"title": "Аналітика найкраще працює з Циклами + Модулями",
|
|
||||||
"description": "Спочатку обмежте роботу в часі через Цикли та згрупуйте робочі одиниці, які тривають довше, у Модулі. Все це в лівому меню."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Призначені вам робочі елементи, розбиті за станом, з’являться тут.",
|
"description": "Призначені вам робочі елементи, розбиті за станом, з’являться тут.",
|
||||||
"title": "Ще немає даних"
|
"title": "Ще немає даних"
|
||||||
|
|
@ -1346,21 +1345,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Створено vs Вирішено",
|
"created_vs_resolved": "Створено vs Вирішено",
|
||||||
"customized_insights": "Персоналізовані аналітичні дані",
|
"customized_insights": "Персоналізовані аналітичні дані",
|
||||||
"backlog_work_items": "Робочі елементи у беклозі",
|
"backlog_work_items": "{entity} у беклозі",
|
||||||
"active_projects": "Активні проєкти",
|
"active_projects": "Активні проєкти",
|
||||||
"trend_on_charts": "Тенденція на графіках",
|
"trend_on_charts": "Тенденція на графіках",
|
||||||
"all_projects": "Усі проєкти",
|
"all_projects": "Усі проєкти",
|
||||||
"summary_of_projects": "Зведення проєктів",
|
"summary_of_projects": "Зведення проєктів",
|
||||||
"project_insights": "Аналітика проєкту",
|
"project_insights": "Аналітика проєкту",
|
||||||
"started_work_items": "Розпочаті робочі елементи",
|
"started_work_items": "Розпочаті {entity}",
|
||||||
"total_work_items": "Усього робочих елементів",
|
"total_work_items": "Усього {entity}",
|
||||||
"total_projects": "Усього проєктів",
|
"total_projects": "Усього проєктів",
|
||||||
"total_admins": "Усього адміністраторів",
|
"total_admins": "Усього адміністраторів",
|
||||||
"total_users": "Усього користувачів",
|
"total_users": "Усього користувачів",
|
||||||
"total_intake": "Загальний дохід",
|
"total_intake": "Загальний дохід",
|
||||||
"un_started_work_items": "Нерозпочаті робочі елементи",
|
"un_started_work_items": "Нерозпочаті {entity}",
|
||||||
"total_guests": "Усього гостей",
|
"total_guests": "Усього гостей",
|
||||||
"completed_work_items": "Завершені робочі елементи"
|
"completed_work_items": "Завершені {entity}",
|
||||||
|
"total": "Усього {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {Проєкт} few {Проєкти} other {Проєктів}}",
|
"label": "{count, plural, one {Проєкт} few {Проєкти} other {Проєктів}}",
|
||||||
|
|
|
||||||
|
|
@ -867,7 +867,19 @@
|
||||||
"view": "Xem",
|
"view": "Xem",
|
||||||
"deactivated_user": "Người dùng bị vô hiệu hóa",
|
"deactivated_user": "Người dùng bị vô hiệu hóa",
|
||||||
"apply": "Áp dụng",
|
"apply": "Áp dụng",
|
||||||
"applying": "Đang áp dụng"
|
"applying": "Đang áp dụng",
|
||||||
|
"users": "Người dùng",
|
||||||
|
"admins": "Quản trị viên",
|
||||||
|
"guests": "Khách",
|
||||||
|
"on_track": "Đúng tiến độ",
|
||||||
|
"off_track": "Chệch hướng",
|
||||||
|
"timeline": "Dòng thời gian",
|
||||||
|
"completion": "Hoàn thành",
|
||||||
|
"upcoming": "Sắp tới",
|
||||||
|
"completed": "Đã hoàn thành",
|
||||||
|
"in_progress": "Đang tiến hành",
|
||||||
|
"planned": "Đã lên kế hoạch",
|
||||||
|
"paused": "Tạm dừng"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "Trục X",
|
"x_axis": "Trục X",
|
||||||
|
|
@ -1317,19 +1329,6 @@
|
||||||
"custom": "Phân tích tùy chỉnh"
|
"custom": "Phân tích tùy chỉnh"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "Theo dõi tiến độ, khối lượng công việc và phân công. Khám phá xu hướng, loại bỏ rào cản và đẩy nhanh công việc",
|
|
||||||
"description": "Xem phạm vi so với nhu cầu, ước tính và mở rộng phạm vi. Nhận hiệu suất của thành viên nhóm và nhóm, đảm bảo dự án của bạn đúng tiến độ.",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "Bắt đầu dự án đầu tiên của bạn",
|
|
||||||
"comic": {
|
|
||||||
"title": "Phân tích hoạt động tốt nhất trong chu kỳ + mô-đun",
|
|
||||||
"description": "Đầu tiên, giới hạn mục công việc của bạn trong chu kỳ và nếu có thể, nhóm mục công việc kéo dài nhiều chu kỳ thành mô-đun. Xem cả hai trong thanh điều hướng bên trái."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "Các hạng mục công việc được giao cho bạn, phân loại theo trạng thái, sẽ hiển thị tại đây.",
|
"description": "Các hạng mục công việc được giao cho bạn, phân loại theo trạng thái, sẽ hiển thị tại đây.",
|
||||||
"title": "Chưa có dữ liệu"
|
"title": "Chưa có dữ liệu"
|
||||||
|
|
@ -1345,21 +1344,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "Đã tạo vs Đã giải quyết",
|
"created_vs_resolved": "Đã tạo vs Đã giải quyết",
|
||||||
"customized_insights": "Thông tin chi tiết tùy chỉnh",
|
"customized_insights": "Thông tin chi tiết tùy chỉnh",
|
||||||
"backlog_work_items": "Các hạng mục công việc tồn đọng",
|
"backlog_work_items": "{entity} tồn đọng",
|
||||||
"active_projects": "Dự án đang hoạt động",
|
"active_projects": "Dự án đang hoạt động",
|
||||||
"trend_on_charts": "Xu hướng trên biểu đồ",
|
"trend_on_charts": "Xu hướng trên biểu đồ",
|
||||||
"all_projects": "Tất cả dự án",
|
"all_projects": "Tất cả dự án",
|
||||||
"summary_of_projects": "Tóm tắt dự án",
|
"summary_of_projects": "Tóm tắt dự án",
|
||||||
"project_insights": "Thông tin chi tiết dự án",
|
"project_insights": "Thông tin chi tiết dự án",
|
||||||
"started_work_items": "Hạng mục công việc đã bắt đầu",
|
"started_work_items": "{entity} đã bắt đầu",
|
||||||
"total_work_items": "Tổng số hạng mục công việc",
|
"total_work_items": "Tổng số {entity}",
|
||||||
"total_projects": "Tổng số dự án",
|
"total_projects": "Tổng số dự án",
|
||||||
"total_admins": "Tổng số quản trị viên",
|
"total_admins": "Tổng số quản trị viên",
|
||||||
"total_users": "Tổng số người dùng",
|
"total_users": "Tổng số người dùng",
|
||||||
"total_intake": "Tổng thu",
|
"total_intake": "Tổng thu",
|
||||||
"un_started_work_items": "Hạng mục công việc chưa bắt đầu",
|
"un_started_work_items": "{entity} chưa bắt đầu",
|
||||||
"total_guests": "Tổng số khách",
|
"total_guests": "Tổng số khách",
|
||||||
"completed_work_items": "Hạng mục công việc đã hoàn thành"
|
"completed_work_items": "{entity} đã hoàn thành",
|
||||||
|
"total": "Tổng số {entity}"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {dự án} other {dự án}}",
|
"label": "{count, plural, one {dự án} other {dự án}}",
|
||||||
|
|
|
||||||
|
|
@ -867,7 +867,19 @@
|
||||||
"view": "查看",
|
"view": "查看",
|
||||||
"deactivated_user": "已停用用户",
|
"deactivated_user": "已停用用户",
|
||||||
"apply": "应用",
|
"apply": "应用",
|
||||||
"applying": "应用中"
|
"applying": "应用中",
|
||||||
|
"users": "用户",
|
||||||
|
"admins": "管理员",
|
||||||
|
"guests": "访客",
|
||||||
|
"on_track": "进展顺利",
|
||||||
|
"off_track": "偏离轨道",
|
||||||
|
"timeline": "时间轴",
|
||||||
|
"completion": "完成",
|
||||||
|
"upcoming": "即将发生",
|
||||||
|
"completed": "已完成",
|
||||||
|
"in_progress": "进行中",
|
||||||
|
"planned": "已计划",
|
||||||
|
"paused": "暂停"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "X轴",
|
"x_axis": "X轴",
|
||||||
|
|
@ -1317,19 +1329,6 @@
|
||||||
"custom": "自定义分析"
|
"custom": "自定义分析"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "跟踪进度、工作量和分配。发现趋势、消除障碍并加快工作进度",
|
|
||||||
"description": "查看范围与需求、估算和范围蔓延。获取团队成员和团队的表现,确保您的项目按时运行。",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "开始您的第一个项目",
|
|
||||||
"comic": {
|
|
||||||
"title": "分析在周期 + 模块中效果最佳",
|
|
||||||
"description": "首先,将您的工作项限定在周期中,如果可能的话,将跨越多个周期的工作项分组到模块中。在左侧导航栏中查看这两项。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "分配给您的工作项将按状态分类显示在此处。",
|
"description": "分配给您的工作项将按状态分类显示在此处。",
|
||||||
"title": "暂无数据"
|
"title": "暂无数据"
|
||||||
|
|
@ -1345,21 +1344,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "已创建 vs 已解决",
|
"created_vs_resolved": "已创建 vs 已解决",
|
||||||
"customized_insights": "自定义洞察",
|
"customized_insights": "自定义洞察",
|
||||||
"backlog_work_items": "待办工作项",
|
"backlog_work_items": "待办的{entity}",
|
||||||
"active_projects": "活跃项目",
|
"active_projects": "活跃项目",
|
||||||
"trend_on_charts": "图表趋势",
|
"trend_on_charts": "图表趋势",
|
||||||
"all_projects": "所有项目",
|
"all_projects": "所有项目",
|
||||||
"summary_of_projects": "项目概览",
|
"summary_of_projects": "项目概览",
|
||||||
"project_insights": "项目洞察",
|
"project_insights": "项目洞察",
|
||||||
"started_work_items": "已开始的工作项",
|
"started_work_items": "已开始的{entity}",
|
||||||
"total_work_items": "工作项总数",
|
"total_work_items": "{entity}总数",
|
||||||
"total_projects": "项目总数",
|
"total_projects": "项目总数",
|
||||||
"total_admins": "管理员总数",
|
"total_admins": "管理员总数",
|
||||||
"total_users": "用户总数",
|
"total_users": "用户总数",
|
||||||
"total_intake": "总收入",
|
"total_intake": "总收入",
|
||||||
"un_started_work_items": "未开始的工作项",
|
"un_started_work_items": "未开始的{entity}",
|
||||||
"total_guests": "访客总数",
|
"total_guests": "访客总数",
|
||||||
"completed_work_items": "已完成的工作项"
|
"completed_work_items": "已完成的{entity}",
|
||||||
|
"total": "{entity}总数"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {项目} other {项目}}",
|
"label": "{count, plural, one {项目} other {项目}}",
|
||||||
|
|
|
||||||
|
|
@ -868,7 +868,19 @@
|
||||||
"view": "檢視",
|
"view": "檢視",
|
||||||
"deactivated_user": "已停用用戶",
|
"deactivated_user": "已停用用戶",
|
||||||
"apply": "應用",
|
"apply": "應用",
|
||||||
"applying": "應用中"
|
"applying": "應用中",
|
||||||
|
"users": "使用者",
|
||||||
|
"admins": "管理員",
|
||||||
|
"guests": "訪客",
|
||||||
|
"on_track": "進展順利",
|
||||||
|
"off_track": "偏離軌道",
|
||||||
|
"timeline": "時間軸",
|
||||||
|
"completion": "完成",
|
||||||
|
"upcoming": "即將發生",
|
||||||
|
"completed": "已完成",
|
||||||
|
"in_progress": "進行中",
|
||||||
|
"planned": "已計劃",
|
||||||
|
"paused": "暫停"
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
"x_axis": "X 軸",
|
"x_axis": "X 軸",
|
||||||
|
|
@ -1318,19 +1330,6 @@
|
||||||
"custom": "自訂分析"
|
"custom": "自訂分析"
|
||||||
},
|
},
|
||||||
"empty_state": {
|
"empty_state": {
|
||||||
"general": {
|
|
||||||
"title": "追蹤進度、工作量和分配。發現趨勢、移除阻礙,加快工作進展",
|
|
||||||
"description": "檢視範圍與需求、評估和範圍擴展。取得團隊成員和團隊的績效,確保您的專案按時進行。",
|
|
||||||
"primary_button": {
|
|
||||||
"text": "開始您的第一個專案",
|
|
||||||
"comic": {
|
|
||||||
"title": "分析最適合搭配週期 + 模組使用",
|
|
||||||
"description": "首先,將您的工作事項時間區段到週期中,如果可以的話,將跨週期的工作事項分組到模組中。請檢視左側導覽列中的兩個功能。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"empty_state_v2": {
|
|
||||||
"customized_insights": {
|
"customized_insights": {
|
||||||
"description": "指派給您的工作項目將依狀態分類顯示在此處。",
|
"description": "指派給您的工作項目將依狀態分類顯示在此處。",
|
||||||
"title": "尚無資料"
|
"title": "尚無資料"
|
||||||
|
|
@ -1346,21 +1345,22 @@
|
||||||
},
|
},
|
||||||
"created_vs_resolved": "已建立 vs 已解決",
|
"created_vs_resolved": "已建立 vs 已解決",
|
||||||
"customized_insights": "自訂化洞察",
|
"customized_insights": "自訂化洞察",
|
||||||
"backlog_work_items": "待辦工作項目",
|
"backlog_work_items": "待辦的{entity}",
|
||||||
"active_projects": "啟用中的專案",
|
"active_projects": "啟用中的專案",
|
||||||
"trend_on_charts": "圖表趨勢",
|
"trend_on_charts": "圖表趨勢",
|
||||||
"all_projects": "所有專案",
|
"all_projects": "所有專案",
|
||||||
"summary_of_projects": "專案摘要",
|
"summary_of_projects": "專案摘要",
|
||||||
"project_insights": "專案洞察",
|
"project_insights": "專案洞察",
|
||||||
"started_work_items": "已開始的工作項目",
|
"started_work_items": "已開始的{entity}",
|
||||||
"total_work_items": "工作項目總數",
|
"total_work_items": "{entity}總數",
|
||||||
"total_projects": "專案總數",
|
"total_projects": "專案總數",
|
||||||
"total_admins": "管理員總數",
|
"total_admins": "管理員總數",
|
||||||
"total_users": "使用者總數",
|
"total_users": "使用者總數",
|
||||||
"total_intake": "總收入",
|
"total_intake": "總收入",
|
||||||
"un_started_work_items": "未開始的工作項目",
|
"un_started_work_items": "未開始的{entity}",
|
||||||
"total_guests": "訪客總數",
|
"total_guests": "訪客總數",
|
||||||
"completed_work_items": "已完成的工作項目"
|
"completed_work_items": "已完成的{entity}",
|
||||||
|
"total": "{entity}總數"
|
||||||
},
|
},
|
||||||
"workspace_projects": {
|
"workspace_projects": {
|
||||||
"label": "{count, plural, one {專案} other {專案}}",
|
"label": "{count, plural, one {專案} other {專案}}",
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
// constants
|
|
||||||
import { API_BASE_URL } from "@plane/constants";
|
|
||||||
// types
|
|
||||||
import {
|
|
||||||
IAnalyticsParams,
|
|
||||||
IAnalyticsResponse,
|
|
||||||
IDefaultAnalyticsResponse,
|
|
||||||
IExportAnalyticsFormData,
|
|
||||||
ISaveAnalyticsFormData,
|
|
||||||
} from "@plane/types";
|
|
||||||
// services
|
|
||||||
import { APIService } from "../api.service";
|
|
||||||
|
|
||||||
export class AnalyticsService extends APIService {
|
|
||||||
constructor(BASE_URL?: string) {
|
|
||||||
super(BASE_URL || API_BASE_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves analytics data for a specific workspace
|
|
||||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
|
||||||
* @param {IAnalyticsParams} params - Parameters for filtering analytics data
|
|
||||||
* @param {string|number} [params.project] - Optional project identifier that will be converted to string
|
|
||||||
* @returns {Promise<IAnalyticsResponse>} The analytics data for the workspace
|
|
||||||
* @throws {Error} Throws response data if the request fails
|
|
||||||
*/
|
|
||||||
async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise<IAnalyticsResponse> {
|
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/analytics/`, {
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
project: params?.project ? params.project.toString() : null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves default analytics data for a workspace
|
|
||||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
|
||||||
* @param {Partial<IAnalyticsParams>} [params] - Optional parameters for filtering default analytics
|
|
||||||
* @param {string|number} [params.project] - Optional project identifier that will be converted to string
|
|
||||||
* @returns {Promise<IDefaultAnalyticsResponse>} The default analytics data
|
|
||||||
* @throws {Error} Throws response data if the request fails
|
|
||||||
*/
|
|
||||||
async getDefaultAnalytics(
|
|
||||||
workspaceSlug: string,
|
|
||||||
params?: Partial<IAnalyticsParams>
|
|
||||||
): Promise<IDefaultAnalyticsResponse> {
|
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/default-analytics/`, {
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
project: params?.project ? params.project.toString() : null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves analytics view configuration for a workspace
|
|
||||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
|
||||||
* @param {ISaveAnalyticsFormData} data - The analytics configuration data to save
|
|
||||||
* @returns {Promise<any>} The response from saving the analytics view
|
|
||||||
* @throws {Error} Throws response data if the request fails
|
|
||||||
*/
|
|
||||||
async save(workspaceSlug: string, data: ISaveAnalyticsFormData): Promise<any> {
|
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/analytic-view/`, data)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exports analytics data for a workspace
|
|
||||||
* @param {string} workspaceSlug - The unique identifier for the workspace
|
|
||||||
* @param {IExportAnalyticsFormData} data - Configuration for the analytics export
|
|
||||||
* @returns {Promise<any>} The exported analytics data
|
|
||||||
* @throws {Error} Throws response data if the request fails
|
|
||||||
*/
|
|
||||||
async export(workspaceSlug: string, data: IExportAnalyticsFormData): Promise<any> {
|
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/export-analytics/`, data)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./analytics.service";
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export * from "./ai";
|
export * from "./ai";
|
||||||
export * from "./analytics";
|
|
||||||
export * from "./developer";
|
export * from "./developer";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./cycle";
|
export * from "./cycle";
|
||||||
|
|
|
||||||
55
packages/types/src/analytics-v2.d.ts
vendored
55
packages/types/src/analytics-v2.d.ts
vendored
|
|
@ -1,55 +0,0 @@
|
||||||
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants";
|
|
||||||
import { TChartData } from "./charts";
|
|
||||||
|
|
||||||
export type TAnalyticsTabsV2Base = "overview" | "work-items";
|
|
||||||
export type TAnalyticsGraphsV2Base = "projects" | "work-items" | "custom-work-items";
|
|
||||||
|
|
||||||
// service types
|
|
||||||
|
|
||||||
export interface IAnalyticsResponseV2 {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAnalyticsResponseFieldsV2 {
|
|
||||||
count: number;
|
|
||||||
filter_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAnalyticsRadarEntityV2 {
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// chart types
|
|
||||||
|
|
||||||
export interface IChartResponseV2 {
|
|
||||||
schema: Record<string, string>;
|
|
||||||
data: TChartData<string, string>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// table types
|
|
||||||
|
|
||||||
export interface WorkItemInsightColumns {
|
|
||||||
project_id?: string;
|
|
||||||
project__name?: string;
|
|
||||||
cancelled_work_items: number;
|
|
||||||
completed_work_items: number;
|
|
||||||
backlog_work_items: number;
|
|
||||||
un_started_work_items: number;
|
|
||||||
started_work_items: number;
|
|
||||||
// because of the peek view, we will display the name of the project instead of project__name
|
|
||||||
display_name?: string;
|
|
||||||
avatar_url?: string;
|
|
||||||
assignee_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnalyticsTableDataMap = {
|
|
||||||
"work-items": WorkItemInsightColumns;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IAnalyticsV2Params {
|
|
||||||
x_axis: ChartXAxisProperty;
|
|
||||||
y_axis: ChartYAxisMetric;
|
|
||||||
group_by?: ChartXAxisProperty;
|
|
||||||
}
|
|
||||||
156
packages/types/src/analytics.d.ts
vendored
156
packages/types/src/analytics.d.ts
vendored
|
|
@ -1,116 +1,60 @@
|
||||||
|
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants";
|
||||||
|
import { TChartData } from "./charts";
|
||||||
|
|
||||||
|
export type TAnalyticsTabsBase = "overview" | "work-items";
|
||||||
|
export type TAnalyticsGraphsBase = "projects" | "work-items" | "custom-work-items";
|
||||||
|
export type TAnalyticsFilterParams = {
|
||||||
|
project_ids?: string;
|
||||||
|
cycle_id?: string;
|
||||||
|
module_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// service types
|
||||||
|
|
||||||
export interface IAnalyticsResponse {
|
export interface IAnalyticsResponse {
|
||||||
total: number;
|
[key: string]: any;
|
||||||
distribution: IAnalyticsData;
|
|
||||||
extras: {
|
|
||||||
assignee_details: IAnalyticsAssigneeDetails[];
|
|
||||||
cycle_details: IAnalyticsCycleDetails[];
|
|
||||||
label_details: IAnalyticsLabelDetails[];
|
|
||||||
module_details: IAnalyticsModuleDetails[];
|
|
||||||
state_details: IAnalyticsStateDetails[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAnalyticsData {
|
export interface IAnalyticsResponseFields {
|
||||||
[key: string]: {
|
count: number;
|
||||||
dimension: string | null;
|
filter_count: number;
|
||||||
segment?: string;
|
|
||||||
count?: number;
|
|
||||||
estimate?: number | null;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAnalyticsAssigneeDetails {
|
export interface IAnalyticsRadarEntity {
|
||||||
assignees__avatar_url: string | null;
|
key: string;
|
||||||
assignees__display_name: string | null;
|
|
||||||
assignees__first_name: string;
|
|
||||||
assignees__id: string | null;
|
|
||||||
assignees__last_name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAnalyticsCycleDetails {
|
|
||||||
issue_cycle__cycle__name: string | null;
|
|
||||||
issue_cycle__cycle_id: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAnalyticsLabelDetails {
|
|
||||||
labels__color: string | null;
|
|
||||||
labels__id: string | null;
|
|
||||||
labels__name: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAnalyticsModuleDetails {
|
|
||||||
issue_module__module__name: string | null;
|
|
||||||
issue_module__module_id: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAnalyticsStateDetails {
|
|
||||||
state__color: string;
|
|
||||||
state__name: string;
|
|
||||||
state_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TXAxisValues =
|
|
||||||
| "state_id"
|
|
||||||
| "state__group"
|
|
||||||
| "labels__id"
|
|
||||||
| "assignees__id"
|
|
||||||
| "estimate_point__value"
|
|
||||||
| "issue_cycle__cycle_id"
|
|
||||||
| "issue_module__module_id"
|
|
||||||
| "priority"
|
|
||||||
| "start_date"
|
|
||||||
| "target_date"
|
|
||||||
| "created_at"
|
|
||||||
| "completed_at";
|
|
||||||
|
|
||||||
export type TYAxisValues = "issue_count" | "estimate";
|
|
||||||
|
|
||||||
export interface IAnalyticsParams {
|
|
||||||
x_axis: TXAxisValues;
|
|
||||||
y_axis: TYAxisValues;
|
|
||||||
segment?: TXAxisValues | null;
|
|
||||||
project?: string[] | null;
|
|
||||||
cycle?: string | null;
|
|
||||||
module?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISaveAnalyticsFormData {
|
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
|
||||||
query_dict: IExportAnalyticsFormData;
|
|
||||||
}
|
|
||||||
export interface IExportAnalyticsFormData {
|
|
||||||
x_axis: TXAxisValues;
|
|
||||||
y_axis: TYAxisValues;
|
|
||||||
segment?: TXAxisValues | null;
|
|
||||||
project?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDefaultAnalyticsUser {
|
|
||||||
assignees__avatar_url: string | null;
|
|
||||||
assignees__first_name: string;
|
|
||||||
assignees__last_name: string;
|
|
||||||
assignees__display_name: string;
|
|
||||||
assignees__id: string;
|
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDefaultAnalyticsResponse {
|
// chart types
|
||||||
issue_completed_month_wise: { month: number; count: number }[];
|
|
||||||
most_issue_closed_user: IDefaultAnalyticsUser[];
|
export interface IChartResponse {
|
||||||
most_issue_created_user: {
|
schema: Record<string, string>;
|
||||||
created_by__avatar_url: string | null;
|
data: TChartData<string, string>[];
|
||||||
created_by__first_name: string;
|
}
|
||||||
created_by__last_name: string;
|
|
||||||
created_by__display_name: string;
|
// table types
|
||||||
created_by__id: string;
|
|
||||||
count: number;
|
export interface WorkItemInsightColumns {
|
||||||
}[];
|
project_id?: string;
|
||||||
open_estimate_sum: number;
|
project__name?: string;
|
||||||
open_issues: number;
|
cancelled_work_items: number;
|
||||||
open_issues_classified: { state_group: string; state_count: number }[];
|
completed_work_items: number;
|
||||||
pending_issue_user: IDefaultAnalyticsUser[];
|
backlog_work_items: number;
|
||||||
total_estimate_sum: number;
|
un_started_work_items: number;
|
||||||
total_issues: number;
|
started_work_items: number;
|
||||||
total_issues_classified: { state_group: string; state_count: number }[];
|
// because of the peek view, we will display the name of the project instead of project__name
|
||||||
|
display_name?: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
assignee_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnalyticsTableDataMap = {
|
||||||
|
"work-items": WorkItemInsightColumns;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IAnalyticsParams {
|
||||||
|
x_axis: ChartXAxisProperty;
|
||||||
|
y_axis: ChartYAxisMetric;
|
||||||
|
group_by?: ChartXAxisProperty;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
packages/types/src/index.d.ts
vendored
2
packages/types/src/index.d.ts
vendored
|
|
@ -43,4 +43,4 @@ export * from "./home";
|
||||||
export * from "./stickies";
|
export * from "./stickies";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./payment";
|
export * from "./payment";
|
||||||
export * from "./analytics-v2";
|
export * from "./analytics";
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { Tabs } from "@plane/ui";
|
import { Tabs } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import AnalyticsFilterActions from "@/components/analytics-v2/analytics-filter-actions";
|
import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions";
|
||||||
import { PageHead } from "@/components/core";
|
import { PageHead } from "@/components/core";
|
||||||
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
|
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-v2/tabs";
|
import { ANALYTICS_TABS } from "@/plane-web/components/analytics/tabs";
|
||||||
|
|
||||||
const AnalyticsPage = observer(() => {
|
const AnalyticsPage = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, ContrastIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
import { Breadcrumbs, Button, ContrastIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal";
|
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||||
import { CycleQuickActions } from "@/components/cycles";
|
import { CycleQuickActions } from "@/components/cycles";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal";
|
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
|
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, IssueLayoutIcon } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
import { isIssueFilterActive } from "@/helpers/filter.helper";
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal";
|
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||||
import {
|
import {
|
||||||
DisplayFiltersSelection,
|
DisplayFiltersSelection,
|
||||||
FilterSelection,
|
FilterSelection,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, Button, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
import { Breadcrumbs, Button, DiceIcon, Tooltip, Header, CustomSearchSelect } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal";
|
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||||
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
import { BreadcrumbLink, SwitcherLabel } from "@/components/common";
|
||||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "@/components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||||
import { WorkItemsModal } from "@/components/analytics-v2/work-items/modal";
|
|
||||||
import {
|
import {
|
||||||
DisplayFiltersSelection,
|
DisplayFiltersSelection,
|
||||||
FilterSelection,
|
FilterSelection,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { TAnalyticsTabsV2Base } from "@plane/types";
|
|
||||||
import { Overview } from "@/components/analytics-v2/overview";
|
|
||||||
import { WorkItems } from "@/components/analytics-v2/work-items";
|
|
||||||
export const ANALYTICS_TABS: {
|
|
||||||
key: TAnalyticsTabsV2Base;
|
|
||||||
i18nKey: string;
|
|
||||||
content: React.FC;
|
|
||||||
}[] = [
|
|
||||||
{ key: "overview", i18nKey: "common.overview", content: Overview },
|
|
||||||
{ key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems },
|
|
||||||
];
|
|
||||||
11
web/ce/components/analytics/tabs.ts
Normal file
11
web/ce/components/analytics/tabs.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
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;
|
||||||
|
}[] = [
|
||||||
|
{ key: "overview", i18nKey: "common.overview", content: Overview },
|
||||||
|
{ key: "work-items", i18nKey: "sidebar.work_items", content: WorkItems },
|
||||||
|
];
|
||||||
|
|
@ -65,22 +65,10 @@ export const SidebarChart: FC<ProgressChartProps> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="relative flex items-center gap-2">
|
|
||||||
<div className="flex items-center justify-center gap-1 text-xs">
|
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
|
||||||
<span>{t("ideal")}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center gap-1 text-xs">
|
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
|
|
||||||
<span>{t("current")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{cycleStartDate && cycleEndDate && completionChartDistributionData ? (
|
{cycleStartDate && cycleEndDate && completionChartDistributionData ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ProgressChart
|
<ProgressChart
|
||||||
distribution={completionChartDistributionData}
|
distribution={completionChartDistributionData}
|
||||||
startDate={cycleStartDate}
|
|
||||||
endDate={cycleEndDate}
|
|
||||||
totalIssues={estimateType === "points" ? totalEstimatePoints : totalIssues}
|
totalIssues={estimateType === "points" ? totalEstimatePoints : totalIssues}
|
||||||
plotTitle={estimateType === "points" ? t("points") : t("work_items")}
|
plotTitle={estimateType === "points" ? t("points") : t("work_items")}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
8
web/ce/store/analytics.store.ts
Normal file
8
web/ce/store/analytics.store.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { BaseAnalyticsStore, IBaseAnalyticsStore } from "@/store/analytics.store";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export interface IAnalyticsStore extends IBaseAnalyticsStore {
|
||||||
|
//observables
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AnalyticsStore extends BaseAnalyticsStore {}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./overview/root";
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { ColumnDef, Row, Table } from "@tanstack/react-table";
|
|
||||||
import { download, generateCsv, mkConfig } from "export-to-csv";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { Download } from "lucide-react";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { AnalyticsTableDataMap, TAnalyticsTabsV2Base } from "@plane/types";
|
|
||||||
import { Button } from "@plane/ui";
|
|
||||||
import { DataTable } from "./data-table";
|
|
||||||
import { TableLoader } from "./loader";
|
|
||||||
interface InsightTableProps<T extends Exclude<TAnalyticsTabsV2Base, "overview">> {
|
|
||||||
analyticsType: T;
|
|
||||||
data?: AnalyticsTableDataMap[T][];
|
|
||||||
isLoading?: boolean;
|
|
||||||
columns: ColumnDef<AnalyticsTableDataMap[T]>[];
|
|
||||||
columnsLabels?: Record<string, string>;
|
|
||||||
headerText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InsightTable = <T extends Exclude<TAnalyticsTabsV2Base, "overview">>(
|
|
||||||
props: InsightTableProps<T>
|
|
||||||
): React.ReactElement => {
|
|
||||||
const { data, isLoading, columns, columnsLabels, headerText } = props;
|
|
||||||
const params = useParams();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const workspaceSlug = params.workspaceSlug.toString();
|
|
||||||
if (isLoading) {
|
|
||||||
return <TableLoader columns={columns} rows={5} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csvConfig = mkConfig({
|
|
||||||
fieldSeparator: ",",
|
|
||||||
filename: `${workspaceSlug}-analytics`,
|
|
||||||
decimalSeparator: ".",
|
|
||||||
useKeysAsHeaders: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const exportCSV = (rows: Row<AnalyticsTableDataMap[T]>[]) => {
|
|
||||||
const rowData: any = rows.map((row) => {
|
|
||||||
const { project_id, avatar_url, assignee_id, ...exportableData } = row.original;
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(exportableData).map(([key, value]) => {
|
|
||||||
if (columnsLabels?.[key]) {
|
|
||||||
return [columnsLabels[key], value];
|
|
||||||
}
|
|
||||||
return [key, value];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const csv = generateCsv(csvConfig)(rowData);
|
|
||||||
download(csvConfig)(csv);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="">
|
|
||||||
{data ? (
|
|
||||||
<DataTable
|
|
||||||
columns={columns}
|
|
||||||
data={data}
|
|
||||||
searchPlaceholder={`${data.length} ${headerText}`}
|
|
||||||
actions={(table: Table<AnalyticsTableDataMap[T]>) => (
|
|
||||||
<Button
|
|
||||||
variant="accent-primary"
|
|
||||||
prependIcon={<Download className="h-3.5 w-3.5" />}
|
|
||||||
onClick={() => exportCSV(table.getFilteredRowModel().rows)}
|
|
||||||
>
|
|
||||||
<div>{t("exporter.csv.short_description")}</div>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div>No data</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
// plane package imports
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { insightsFields } from "@plane/constants";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { IAnalyticsResponseV2, TAnalyticsTabsV2Base } from "@plane/types";
|
|
||||||
//hooks
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2";
|
|
||||||
//services
|
|
||||||
import { AnalyticsV2Service } from "@/services/analytics-v2.service";
|
|
||||||
// plane web components
|
|
||||||
import InsightCard from "./insight-card";
|
|
||||||
|
|
||||||
const analyticsV2Service = new AnalyticsV2Service();
|
|
||||||
|
|
||||||
const TotalInsights: React.FC<{ analyticsType: TAnalyticsTabsV2Base; peekView?: boolean }> = observer(
|
|
||||||
({ analyticsType, peekView }) => {
|
|
||||||
const params = useParams();
|
|
||||||
const workspaceSlug = params.workspaceSlug.toString();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { selectedDuration, selectedProjects, selectedDurationLabel, selectedCycle, selectedModule, isPeekView } =
|
|
||||||
useAnalyticsV2();
|
|
||||||
|
|
||||||
const { data: totalInsightsData, isLoading } = useSWR(
|
|
||||||
`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
|
||||||
() =>
|
|
||||||
analyticsV2Service.getAdvanceAnalytics<IAnalyticsResponseV2>(
|
|
||||||
workspaceSlug,
|
|
||||||
analyticsType,
|
|
||||||
{
|
|
||||||
// date_filter: selectedDuration,
|
|
||||||
...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}),
|
|
||||||
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
|
|
||||||
...(selectedModule ? { module_id: selectedModule } : {}),
|
|
||||||
},
|
|
||||||
isPeekView
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"grid grid-cols-1 gap-8 sm:grid-cols-2 md:gap-10",
|
|
||||||
!peekView
|
|
||||||
? insightsFields[analyticsType].length % 5 === 0
|
|
||||||
? "gap-10 lg:grid-cols-5"
|
|
||||||
: "gap-8 lg:grid-cols-4"
|
|
||||||
: "grid-cols-2"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{insightsFields[analyticsType]?.map((item: string) => (
|
|
||||||
<InsightCard
|
|
||||||
key={`${analyticsType}-${item}`}
|
|
||||||
isLoading={isLoading}
|
|
||||||
data={totalInsightsData?.[item]}
|
|
||||||
label={t(`workspace_analytics.${item}`)}
|
|
||||||
versus={selectedDurationLabel}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default TotalInsights;
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject } from "@/hooks/store";
|
import { useProject } from "@/hooks/store";
|
||||||
import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2";
|
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||||
// components
|
// components
|
||||||
import DurationDropdown from "./select/duration";
|
import DurationDropdown from "./select/duration";
|
||||||
import { ProjectSelect } from "./select/project";
|
import { ProjectSelect } from "./select/project";
|
||||||
|
|
||||||
const AnalyticsFilterActions = observer(() => {
|
const AnalyticsFilterActions = observer(() => {
|
||||||
const { selectedProjects, selectedDuration, updateSelectedProjects, updateSelectedDuration } = useAnalyticsV2();
|
const { selectedProjects, selectedDuration, updateSelectedProjects, updateSelectedDuration } = useAnalytics();
|
||||||
const { workspaceProjectIds } = useProject();
|
const { workspaceProjectIds } = useProject();
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
9
web/core/components/analytics/config.ts
Normal file
9
web/core/components/analytics/config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { mkConfig } from "export-to-csv";
|
||||||
|
|
||||||
|
export const csvConfig = (workspaceSlug: string) =>
|
||||||
|
mkConfig({
|
||||||
|
fieldSeparator: ",",
|
||||||
|
filename: `${workspaceSlug}-analytics`,
|
||||||
|
decimalSeparator: ".",
|
||||||
|
useKeysAsHeaders: true,
|
||||||
|
});
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { IAnalyticsParams } from "@plane/types";
|
|
||||||
// services
|
|
||||||
// components
|
|
||||||
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "@/components/analytics";
|
|
||||||
// types
|
|
||||||
// fetch-keys
|
|
||||||
import { ANALYTICS } from "@/constants/fetch-keys";
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
import { useAppTheme } from "@/hooks/store";
|
|
||||||
import { hideFloatingBot, showFloatingBot } from "@/plane-web/helpers/pi-chat.helper";
|
|
||||||
import { AnalyticsService } from "@/services/analytics.service";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
additionalParams?: Partial<IAnalyticsParams>;
|
|
||||||
fullScreen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: IAnalyticsParams = {
|
|
||||||
x_axis: "priority",
|
|
||||||
y_axis: "issue_count",
|
|
||||||
segment: null,
|
|
||||||
project: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const analyticsService = new AnalyticsService();
|
|
||||||
|
|
||||||
export const CustomAnalytics: React.FC<Props> = observer((props) => {
|
|
||||||
const { additionalParams, fullScreen } = props;
|
|
||||||
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
|
||||||
|
|
||||||
const { control, watch, setValue } = useForm({ defaultValues });
|
|
||||||
|
|
||||||
const params: IAnalyticsParams = {
|
|
||||||
x_axis: watch("x_axis"),
|
|
||||||
y_axis: watch("y_axis"),
|
|
||||||
segment: watch("segment"),
|
|
||||||
project: projectId ? [projectId.toString()] : watch("project"),
|
|
||||||
...additionalParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data: analytics, error: analyticsError } = useSWR(
|
|
||||||
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
|
|
||||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { workspaceAnalyticsSidebarCollapsed } = useAppTheme();
|
|
||||||
|
|
||||||
const isProjectLevel = projectId ? true : false;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
hideFloatingBot();
|
|
||||||
return () => {
|
|
||||||
showFloatingBot();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn("relative flex h-full w-full overflow-hidden", isProjectLevel ? "flex-col-reverse" : "")}>
|
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
|
||||||
<CustomAnalyticsSelectBar
|
|
||||||
control={control}
|
|
||||||
setValue={setValue}
|
|
||||||
params={params}
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
isProjectLevel={isProjectLevel}
|
|
||||||
/>
|
|
||||||
<CustomAnalyticsMainContent
|
|
||||||
analytics={analytics}
|
|
||||||
error={analyticsError}
|
|
||||||
params={params}
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"border-l border-custom-border-200 transition-all",
|
|
||||||
!isProjectLevel
|
|
||||||
? "absolute bottom-0 right-0 top-0 h-full max-w-[250px] flex-shrink-0 sm:max-w-full md:relative"
|
|
||||||
: ""
|
|
||||||
)}
|
|
||||||
style={workspaceAnalyticsSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
|
||||||
>
|
|
||||||
<CustomAnalyticsSidebar analytics={analytics} params={params} isProjectLevel={isProjectLevel} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// nivo
|
|
||||||
import { BarTooltipProps } from "@nivo/bar";
|
|
||||||
// plane imports
|
|
||||||
import { ANALYTICS_DATE_KEYS } from "@plane/constants";
|
|
||||||
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
|
|
||||||
// helpers
|
|
||||||
import { renderMonthAndYear } from "@/helpers/analytics.helper";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
datum: BarTooltipProps<any>;
|
|
||||||
analytics: IAnalyticsResponse;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) => {
|
|
||||||
let tooltipValue: string | number = "";
|
|
||||||
|
|
||||||
const renderAssigneeName = (assigneeId: string): string => {
|
|
||||||
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__id === assigneeId);
|
|
||||||
|
|
||||||
if (!assignee) return "No assignee";
|
|
||||||
|
|
||||||
return assignee.assignees__display_name || "No assignee";
|
|
||||||
};
|
|
||||||
|
|
||||||
if (params.segment) {
|
|
||||||
if (ANALYTICS_DATE_KEYS.includes(params.segment)) tooltipValue = renderMonthAndYear(datum.id);
|
|
||||||
else if (params.segment === "labels__id") {
|
|
||||||
const label = analytics.extras.label_details.find((l) => l.labels__id === datum.id);
|
|
||||||
tooltipValue = label && label.labels__name ? label.labels__name : "None";
|
|
||||||
} else if (params.segment === "state_id") {
|
|
||||||
const state = analytics.extras.state_details.find((s) => s.state_id === datum.id);
|
|
||||||
tooltipValue = state && state.state__name ? state.state__name : "None";
|
|
||||||
} else if (params.segment === "issue_cycle__cycle_id") {
|
|
||||||
const cycle = analytics.extras.cycle_details.find((c) => c.issue_cycle__cycle_id === datum.id);
|
|
||||||
tooltipValue = cycle && cycle.issue_cycle__cycle__name ? cycle.issue_cycle__cycle__name : "None";
|
|
||||||
} else if (params.segment === "issue_module__module_id") {
|
|
||||||
const selectedModule = analytics.extras.module_details.find((m) => m.issue_module__module_id === datum.id);
|
|
||||||
tooltipValue =
|
|
||||||
selectedModule && selectedModule.issue_module__module__name
|
|
||||||
? selectedModule.issue_module__module__name
|
|
||||||
: "None";
|
|
||||||
} else tooltipValue = datum.id;
|
|
||||||
} else {
|
|
||||||
if (ANALYTICS_DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
|
|
||||||
else tooltipValue = datum.id === "count" ? "Work item count" : "Estimate";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
|
||||||
<span
|
|
||||||
className="h-3 w-3 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: datum.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`font-medium text-custom-text-200 ${
|
|
||||||
params.segment
|
|
||||||
? params.segment === "priority" || params.segment === "state__group"
|
|
||||||
? "capitalize"
|
|
||||||
: ""
|
|
||||||
: params.x_axis === "priority" || params.x_axis === "state__group"
|
|
||||||
? "capitalize"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}:
|
|
||||||
</span>
|
|
||||||
<span>{datum.value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
// nivo
|
|
||||||
import { BarDatum } from "@nivo/bar";
|
|
||||||
// components
|
|
||||||
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
|
|
||||||
import { Tooltip } from "@plane/ui";
|
|
||||||
// ui
|
|
||||||
import { BarGraph } from "@/components/ui";
|
|
||||||
// helpers
|
|
||||||
import { generateBarColor, generateDisplayName, renderChartDynamicLabel } from "@/helpers/analytics.helper";
|
|
||||||
import { findStringWithMostCharacters } from "@/helpers/array.helper";
|
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
|
||||||
// types
|
|
||||||
import { CustomTooltip } from "./custom-tooltip";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
analytics: IAnalyticsResponse;
|
|
||||||
barGraphData: {
|
|
||||||
data: BarDatum[];
|
|
||||||
xAxisKeys: string[];
|
|
||||||
};
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
yAxisKey: "count" | "estimate";
|
|
||||||
fullScreen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey, fullScreen }) => {
|
|
||||||
const generateYAxisTickValues = () => {
|
|
||||||
if (!analytics) return [];
|
|
||||||
|
|
||||||
let data: number[] = [];
|
|
||||||
|
|
||||||
if (params.segment)
|
|
||||||
// find the total no of work items in each segment
|
|
||||||
data = Object.keys(analytics.distribution).map((segment) => {
|
|
||||||
let totalSegmentIssues = 0;
|
|
||||||
|
|
||||||
analytics.distribution[segment].map((s) => {
|
|
||||||
totalSegmentIssues += s[yAxisKey] as number;
|
|
||||||
});
|
|
||||||
|
|
||||||
return totalSegmentIssues;
|
|
||||||
});
|
|
||||||
else data = barGraphData.data.map((d) => d[yAxisKey] as number);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const longestXAxisLabel = findStringWithMostCharacters(barGraphData.data.map((d) => `${d.name}`));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BarGraph
|
|
||||||
data={barGraphData.data}
|
|
||||||
indexBy="name"
|
|
||||||
keys={barGraphData.xAxisKeys}
|
|
||||||
colors={(datum) =>
|
|
||||||
generateBarColor(
|
|
||||||
params.segment ? `${datum.id}` : `${datum.indexValue}`,
|
|
||||||
analytics,
|
|
||||||
params,
|
|
||||||
params.segment ? "segment" : "x_axis"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
customYAxisTickValues={generateYAxisTickValues()}
|
|
||||||
tooltip={(datum) => <CustomTooltip datum={datum} analytics={analytics} params={params} />}
|
|
||||||
height={fullScreen ? "400px" : "300px"}
|
|
||||||
margin={{
|
|
||||||
right: 20,
|
|
||||||
bottom: params.x_axis === "assignees__id" ? 50 : renderChartDynamicLabel(longestXAxisLabel)?.length * 5 + 20,
|
|
||||||
}}
|
|
||||||
axisBottom={{
|
|
||||||
tickSize: 0,
|
|
||||||
tickPadding: 10,
|
|
||||||
tickRotation: barGraphData.data.length > 7 ? -45 : 0,
|
|
||||||
renderTick:
|
|
||||||
params.x_axis === "assignees__id"
|
|
||||||
? (datum) => {
|
|
||||||
const assignee = analytics.extras.assignee_details?.find((a) => a?.assignees__id === datum?.value);
|
|
||||||
|
|
||||||
if (assignee?.assignees__avatar_url && assignee?.assignees__avatar_url !== "")
|
|
||||||
return (
|
|
||||||
<Tooltip tooltipContent={assignee?.assignees__display_name}>
|
|
||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
|
||||||
<image
|
|
||||||
x={-8}
|
|
||||||
y={10}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
xlinkHref={getFileURL(assignee?.assignees__avatar_url)}
|
|
||||||
style={{ clipPath: "circle(50%)" }}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
else
|
|
||||||
return (
|
|
||||||
<Tooltip tooltipContent={assignee?.assignees__display_name}>
|
|
||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
|
||||||
<circle cy={18} r={8} fill="#374151" />
|
|
||||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
|
||||||
{params.x_axis === "assignees__id"
|
|
||||||
? datum.value && datum.value !== "None"
|
|
||||||
? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase()
|
|
||||||
: "?"
|
|
||||||
: datum.value && datum.value !== "None"
|
|
||||||
? `${datum.value}`.toUpperCase()[0]
|
|
||||||
: "?"}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: (datum) => (
|
|
||||||
<Tooltip tooltipContent={generateDisplayName(datum.value, analytics, params, "x_axis")}>
|
|
||||||
<g transform={`translate(${datum.x},${datum.y + 20})`}>
|
|
||||||
<text
|
|
||||||
x={0}
|
|
||||||
y={datum.y}
|
|
||||||
textAnchor={`${barGraphData.data.length > 7 ? "end" : "middle"}`}
|
|
||||||
fontSize={10}
|
|
||||||
fill="rgb(var(--color-text-200))"
|
|
||||||
className={`${barGraphData.data.length > 7 ? "-rotate-45" : ""}`}
|
|
||||||
>
|
|
||||||
{renderChartDynamicLabel(generateDisplayName(datum.value, analytics, params, "x_axis"))?.label}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
theme={{
|
|
||||||
axis: {},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export * from "./graph";
|
|
||||||
export * from "./select";
|
|
||||||
export * from "./custom-analytics";
|
|
||||||
export * from "./main-content";
|
|
||||||
export * from "./select-bar";
|
|
||||||
export * from "./sidebar";
|
|
||||||
export * from "./table";
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { mutate } from "swr";
|
|
||||||
// types
|
|
||||||
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { Button, Loader } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { AnalyticsGraph, AnalyticsTable } from "@/components/analytics";
|
|
||||||
// fetch-keys
|
|
||||||
import { ANALYTICS } from "@/constants/fetch-keys";
|
|
||||||
// helpers
|
|
||||||
import { convertResponseToBarGraphData } from "@/helpers/analytics.helper";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
analytics: IAnalyticsResponse | undefined;
|
|
||||||
error: any;
|
|
||||||
fullScreen: boolean;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomAnalyticsMainContent: React.FC<Props> = (props) => {
|
|
||||||
const { analytics, error, fullScreen, params } = props;
|
|
||||||
|
|
||||||
const { workspaceSlug } = useParams();
|
|
||||||
|
|
||||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate";
|
|
||||||
const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!error ? (
|
|
||||||
analytics ? (
|
|
||||||
analytics.total > 0 ? (
|
|
||||||
<div className="h-full overflow-y-auto vertical-scrollbar scrollbar-md">
|
|
||||||
<AnalyticsGraph
|
|
||||||
analytics={analytics}
|
|
||||||
barGraphData={barGraphData}
|
|
||||||
params={params}
|
|
||||||
yAxisKey={yAxisKey}
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
/>
|
|
||||||
<AnalyticsTable analytics={analytics} barGraphData={barGraphData} params={params} yAxisKey={yAxisKey} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid h-full place-items-center p-5">
|
|
||||||
<div className="space-y-4 text-custom-text-200">
|
|
||||||
<p className="text-sm">No matching work items found. Try changing the parameters.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Loader className="space-y-6">
|
|
||||||
<Loader.Item height="300px" />
|
|
||||||
<Loader className="space-y-4">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
</Loader>
|
|
||||||
</Loader>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="grid h-full place-items-center p-5">
|
|
||||||
<div className="space-y-4 text-custom-text-200">
|
|
||||||
<p className="text-sm">There was some error in fetching the data.</p>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
|
||||||
// plane imports
|
|
||||||
import { ANALYTICS_X_AXIS_VALUES } from "@plane/constants";
|
|
||||||
import { IAnalyticsParams } from "@plane/types";
|
|
||||||
import { Row } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "@/components/analytics";
|
|
||||||
// hooks
|
|
||||||
import { useProject } from "@/hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
control: Control<IAnalyticsParams, any>;
|
|
||||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
fullScreen: boolean;
|
|
||||||
isProjectLevel: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
|
|
||||||
const { control, setValue, params, fullScreen, isProjectLevel } = props;
|
|
||||||
|
|
||||||
const { workspaceProjectIds: workspaceProjectIds, currentProjectDetails } = useProject();
|
|
||||||
|
|
||||||
const analyticsOptions = isProjectLevel
|
|
||||||
? ANALYTICS_X_AXIS_VALUES.filter((v) => {
|
|
||||||
if (v.value === "issue_cycle__cycle_id" && !currentProjectDetails?.cycle_view) return false;
|
|
||||||
if (v.value === "issue_module__module_id" && !currentProjectDetails?.module_view) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
: ANALYTICS_X_AXIS_VALUES;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row
|
|
||||||
className={`grid items-center gap-4 py-2.5 ${
|
|
||||||
isProjectLevel ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-2"
|
|
||||||
} ${fullScreen ? "md:py-5 lg:grid-cols-4" : ""}`}
|
|
||||||
>
|
|
||||||
{!isProjectLevel && (
|
|
||||||
<div>
|
|
||||||
<h6 className="text-xs text-custom-text-200 mb-2">Project</h6>
|
|
||||||
<Controller
|
|
||||||
name="project"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SelectProject
|
|
||||||
value={value ?? undefined}
|
|
||||||
onChange={onChange}
|
|
||||||
projectIds={workspaceProjectIds ?? undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<h6 className="text-xs text-custom-text-200 mb-2">Measure (y-axis)</h6>
|
|
||||||
<Controller
|
|
||||||
name="y_axis"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => <SelectYAxis value={value} onChange={onChange} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h6 className="text-xs text-custom-text-200 mb-2">Dimension (x-axis)</h6>
|
|
||||||
<Controller
|
|
||||||
name="x_axis"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SelectXAxis
|
|
||||||
value={value}
|
|
||||||
onChange={(val: string) => {
|
|
||||||
if (params.segment === val) setValue("segment", null);
|
|
||||||
|
|
||||||
onChange(val);
|
|
||||||
}}
|
|
||||||
params={params}
|
|
||||||
analyticsOptions={analyticsOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h6 className="text-xs text-custom-text-200 mb-2">Group</h6>
|
|
||||||
<Controller
|
|
||||||
name="segment"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SelectSegment value={value} onChange={onChange} params={params} analyticsOptions={analyticsOptions} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from "./project";
|
|
||||||
export * from "./segment";
|
|
||||||
export * from "./x-axis";
|
|
||||||
export * from "./y-axis";
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
// hooks
|
|
||||||
import { CustomSearchSelect } from "@plane/ui";
|
|
||||||
import { useProject } from "@/hooks/store";
|
|
||||||
// ui
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: string[] | undefined;
|
|
||||||
onChange: (val: string[] | null) => void;
|
|
||||||
projectIds: string[] | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectProject: React.FC<Props> = observer((props) => {
|
|
||||||
const { value, onChange, projectIds } = props;
|
|
||||||
const { getProjectById } = useProject();
|
|
||||||
|
|
||||||
const options = projectIds?.map((projectId) => {
|
|
||||||
const projectDetails = getProjectById(projectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: projectDetails?.id,
|
|
||||||
query: `${projectDetails?.name} ${projectDetails?.identifier}`,
|
|
||||||
content: (
|
|
||||||
<div className="flex items-center gap-2 max-w-[300px]">
|
|
||||||
<span className="text-[0.65rem] text-custom-text-200 flex-shrink-0">{projectDetails?.identifier}</span>
|
|
||||||
<span className="flex-grow truncate">{projectDetails?.name}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomSearchSelect
|
|
||||||
value={value ?? []}
|
|
||||||
onChange={(val: string[]) => onChange(val)}
|
|
||||||
options={options}
|
|
||||||
label={
|
|
||||||
<div className="truncate">
|
|
||||||
{value && value.length > 0
|
|
||||||
? projectIds
|
|
||||||
?.filter((p) => value.includes(p))
|
|
||||||
.map((p) => getProjectById(p)?.name)
|
|
||||||
.join(", ")
|
|
||||||
: "All projects"}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { IAnalyticsParams, TXAxisValues } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { CustomSelect } from "@plane/ui";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: TXAxisValues | null | undefined;
|
|
||||||
onChange: () => void;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
analyticsOptions: { value: TXAxisValues; label: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectSegment: React.FC<Props> = ({ value, onChange, params, analyticsOptions }) => {
|
|
||||||
const { cycleId, moduleId } = useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
{analyticsOptions.find((v) => v.value === value)?.label ?? (
|
|
||||||
<span className="text-custom-text-200">No value</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
maxHeight="lg"
|
|
||||||
>
|
|
||||||
<CustomSelect.Option value={null}>No value</CustomSelect.Option>
|
|
||||||
{analyticsOptions.map((item) => {
|
|
||||||
if (params.x_axis === item.value) return null;
|
|
||||||
if (cycleId && item.value === "issue_cycle__cycle_id") return null;
|
|
||||||
if (moduleId && item.value === "issue_module__module_id") return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CustomSelect>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { IAnalyticsParams, TXAxisValues } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { CustomSelect } from "@plane/ui";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: TXAxisValues;
|
|
||||||
onChange: (val: string) => void;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
analyticsOptions: { value: TXAxisValues; label: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectXAxis: React.FC<Props> = (props) => {
|
|
||||||
const { value, onChange, params, analyticsOptions } = props;
|
|
||||||
|
|
||||||
const { cycleId, moduleId } = useParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
label={<span>{analyticsOptions.find((v) => v.value === value)?.label}</span>}
|
|
||||||
onChange={onChange}
|
|
||||||
maxHeight="lg"
|
|
||||||
>
|
|
||||||
{analyticsOptions.map((item) => {
|
|
||||||
if (params.segment === item.value) return null;
|
|
||||||
if (cycleId && item.value === "issue_cycle__cycle_id") return null;
|
|
||||||
if (moduleId && item.value === "issue_module__module_id") return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CustomSelect>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
// plane imports
|
|
||||||
import { ANALYTICS_Y_AXIS_VALUES } from "@plane/constants";
|
|
||||||
import { TYAxisValues } from "@plane/types";
|
|
||||||
import { CustomSelect } from "@plane/ui";
|
|
||||||
// hooks
|
|
||||||
import { useProjectEstimates } from "@/hooks/store";
|
|
||||||
// plane web constants
|
|
||||||
import { EEstimateSystem } from "@/plane-web/constants/estimates";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: TYAxisValues;
|
|
||||||
onChange: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectYAxis: React.FC<Props> = observer(({ value, onChange }) => {
|
|
||||||
// hooks
|
|
||||||
const { projectId } = useParams();
|
|
||||||
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
|
||||||
|
|
||||||
const isEstimateEnabled = (analyticsOption: string) => {
|
|
||||||
if (analyticsOption === "estimate") {
|
|
||||||
if (
|
|
||||||
projectId &&
|
|
||||||
currentActiveEstimateId &&
|
|
||||||
areEstimateEnabledByProjectId(projectId.toString()) &&
|
|
||||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
label={<span>{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}</span>}
|
|
||||||
onChange={onChange}
|
|
||||||
maxHeight="lg"
|
|
||||||
>
|
|
||||||
{ANALYTICS_Y_AXIS_VALUES.map(
|
|
||||||
(item) =>
|
|
||||||
isEstimateEnabled(item.value) && (
|
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</CustomSelect.Option>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CustomSelect>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./projects-list";
|
|
||||||
export * from "./sidebar-header";
|
|
||||||
export * from "./sidebar";
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
import { observer } from "mobx-react";
|
|
||||||
// icons
|
|
||||||
import { Contrast, LayoutGrid, Users, Loader as Spinner } from "lucide-react";
|
|
||||||
// plane imports
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { Loader } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { Logo } from "@/components/common";
|
|
||||||
// helpers
|
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
|
||||||
// hooks
|
|
||||||
import { useProject } from "@/hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
projectIds: string[];
|
|
||||||
isLoading: boolean;
|
|
||||||
isUpdating: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((props) => {
|
|
||||||
const { projectIds, isLoading, isUpdating } = props;
|
|
||||||
// store hooks
|
|
||||||
const { getProjectById, getProjectAnalyticsCountById } = useProject();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex flex-col gap-4 h-full">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<h4 className="font-medium">{t("workspace_analytics.selected_projects")}</h4>
|
|
||||||
{isUpdating && <Spinner className="animate-spin size-3" />}
|
|
||||||
</div>
|
|
||||||
<div className="relative space-y-6 overflow-hidden overflow-y-auto vertical-scrollbar scrollbar-md">
|
|
||||||
{projectIds.map((projectId) => {
|
|
||||||
const project = getProjectById(projectId);
|
|
||||||
const projectAnalyticsCount = getProjectAnalyticsCountById(projectId);
|
|
||||||
|
|
||||||
if (!project) return;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={projectId} className="w-full">
|
|
||||||
<div className="flex items-center gap-1 text-sm">
|
|
||||||
<div className="h-6 w-6 grid place-items-center">
|
|
||||||
<Logo logo={project.logo_props} />
|
|
||||||
</div>
|
|
||||||
<h5 className="flex items-center gap-1">
|
|
||||||
<p className="break-words">{truncateText(project.name, 20)}</p>
|
|
||||||
<span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 w-full space-y-3 px-2">
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader className="space-y-3">
|
|
||||||
<Loader.Item height="16px" />
|
|
||||||
<Loader.Item height="16px" />
|
|
||||||
<Loader.Item height="16px" />
|
|
||||||
</Loader>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Users className="text-custom-text-200" size={14} strokeWidth={2} />
|
|
||||||
<h6>{t("workspace_analytics.total_members")}</h6>
|
|
||||||
</div>
|
|
||||||
<span className="text-custom-text-200">{projectAnalyticsCount?.total_members}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
|
|
||||||
<h6>{t("workspace_analytics.total_cycles")}</h6>
|
|
||||||
</div>
|
|
||||||
<span className="text-custom-text-200">{projectAnalyticsCount?.total_cycles}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
|
|
||||||
<h6>{t("workspace_analytics.total_modules")}</h6>
|
|
||||||
</div>
|
|
||||||
<span className="text-custom-text-200">{projectAnalyticsCount?.total_modules}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
// components
|
|
||||||
import { NETWORK_CHOICES } from "@plane/constants";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { Logo } from "@/components/common";
|
|
||||||
// constants
|
|
||||||
// helpers
|
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
|
||||||
// hooks
|
|
||||||
import { useCycle, useMember, useModule, useProject } from "@/hooks/store";
|
|
||||||
|
|
||||||
export const CustomAnalyticsSidebarHeader = observer(() => {
|
|
||||||
const { projectId, cycleId, moduleId } = useParams();
|
|
||||||
|
|
||||||
const { getProjectById } = useProject();
|
|
||||||
const { getCycleById } = useCycle();
|
|
||||||
const { getModuleById } = useModule();
|
|
||||||
const { getUserDetails } = useMember();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
|
||||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
|
||||||
const projectDetails = projectId ? getProjectById(projectId.toString()) : undefined;
|
|
||||||
const cycleOwnerDetails = cycleDetails ? getUserDetails(cycleDetails.owned_by_id) : undefined;
|
|
||||||
const moduleLeadDetails = moduleDetails && moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{projectId ? (
|
|
||||||
cycleDetails ? (
|
|
||||||
<div className="h-full overflow-y-auto">
|
|
||||||
<h4 className="break-words font-medium">Analytics for {cycleDetails.name}</h4>
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Lead</h6>
|
|
||||||
<span>{cycleOwnerDetails?.display_name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Start Date</h6>
|
|
||||||
<span>
|
|
||||||
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
|
||||||
? renderFormattedDate(cycleDetails.start_date)
|
|
||||||
: "No start date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Target Date</h6>
|
|
||||||
<span>
|
|
||||||
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
|
||||||
? renderFormattedDate(cycleDetails.end_date)
|
|
||||||
: "No end date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : moduleDetails ? (
|
|
||||||
<div className="h-full overflow-y-auto">
|
|
||||||
<h4 className="break-words font-medium">Analytics for {moduleDetails.name}</h4>
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Lead</h6>
|
|
||||||
{moduleLeadDetails && <span>{moduleLeadDetails?.display_name}</span>}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Start Date</h6>
|
|
||||||
<span>
|
|
||||||
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
|
||||||
? renderFormattedDate(moduleDetails.start_date)
|
|
||||||
: "No start date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Target Date</h6>
|
|
||||||
<span>
|
|
||||||
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
|
||||||
? renderFormattedDate(moduleDetails.target_date)
|
|
||||||
: "No end date"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-full overflow-y-auto">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{projectDetails && (
|
|
||||||
<span className="h-6 w-6 grid place-items-center flex-shrink-0">
|
|
||||||
<Logo logo={projectDetails.logo_props} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<h4 className="break-words font-medium">{projectDetails?.name}</h4>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<h6 className="text-custom-text-200">Network</h6>
|
|
||||||
<span>{t(NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.i18n_label ?? "")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
// icons
|
|
||||||
import { CalendarDays, Download, RefreshCw } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
|
|
||||||
// ui
|
|
||||||
import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { CustomAnalyticsSidebarHeader, CustomAnalyticsSidebarProjectsList } from "@/components/analytics";
|
|
||||||
// constants
|
|
||||||
import { ANALYTICS } from "@/constants/fetch-keys";
|
|
||||||
// helpers
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
|
||||||
// hooks
|
|
||||||
import { useCycle, useModule, useProject, useWorkspace, useUser } from "@/hooks/store";
|
|
||||||
// services
|
|
||||||
import { AnalyticsService } from "@/services/analytics.service";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
analytics: IAnalyticsResponse | undefined;
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
isProjectLevel: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const analyticsService = new AnalyticsService();
|
|
||||||
|
|
||||||
const PROJECT_ANALYTICS_COUNT_PARAMS = {
|
|
||||||
fields: "total_members,total_cycles,total_modules",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|
||||||
const { analytics, params, isProjectLevel = false } = props;
|
|
||||||
// router
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = useParams();
|
|
||||||
// store hooks
|
|
||||||
const { data: currentUser } = useUser();
|
|
||||||
const { workspaceProjectIds, getProjectById, fetchProjectAnalyticsCount } = useProject();
|
|
||||||
const { getWorkspaceById } = useWorkspace();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { fetchCycleDetails, getCycleById } = useCycle();
|
|
||||||
const { fetchModuleDetails, getModuleById } = useModule();
|
|
||||||
// fetch project analytics count
|
|
||||||
const { isLoading: isProjectAnalyticsLoading, isValidating: isProjectAnalyticsUpdating } = useSWR(
|
|
||||||
workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null,
|
|
||||||
workspaceSlug ? () => fetchProjectAnalyticsCount(workspaceSlug.toString(), PROJECT_ANALYTICS_COUNT_PARAMS) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectDetails = projectId ? (getProjectById(projectId.toString()) ?? undefined) : undefined;
|
|
||||||
|
|
||||||
const trackExportAnalytics = () => {
|
|
||||||
if (!currentUser) return;
|
|
||||||
|
|
||||||
const eventPayload: any = {
|
|
||||||
workspaceSlug: workspaceSlug?.toString(),
|
|
||||||
params: {
|
|
||||||
x_axis: params.x_axis,
|
|
||||||
y_axis: params.y_axis,
|
|
||||||
group: params.segment,
|
|
||||||
project: params.project,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (projectDetails) {
|
|
||||||
const workspaceDetails = projectDetails.workspace as IWorkspace;
|
|
||||||
|
|
||||||
eventPayload.workspaceId = workspaceDetails.id;
|
|
||||||
eventPayload.workspaceName = workspaceDetails.name;
|
|
||||||
eventPayload.projectId = projectDetails.id;
|
|
||||||
eventPayload.projectIdentifier = projectDetails.identifier;
|
|
||||||
eventPayload.projectName = projectDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cycleDetails || moduleDetails) {
|
|
||||||
const details = cycleDetails || moduleDetails;
|
|
||||||
|
|
||||||
const currentProjectDetails = getProjectById(details?.project_id || "");
|
|
||||||
const currentWorkspaceDetails = getWorkspaceById(details?.workspace_id || "");
|
|
||||||
|
|
||||||
eventPayload.workspaceId = details?.workspace_id;
|
|
||||||
eventPayload.workspaceName = currentWorkspaceDetails?.name;
|
|
||||||
eventPayload.projectId = details?.project_id;
|
|
||||||
eventPayload.projectIdentifier = currentProjectDetails?.identifier;
|
|
||||||
eventPayload.projectName = currentProjectDetails?.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cycleDetails) {
|
|
||||||
eventPayload.cycleId = cycleDetails.id;
|
|
||||||
eventPayload.cycleName = cycleDetails.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moduleDetails) {
|
|
||||||
eventPayload.moduleId = moduleDetails.id;
|
|
||||||
eventPayload.moduleName = moduleDetails.name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportAnalytics = () => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
const data: IExportAnalyticsFormData = {
|
|
||||||
x_axis: params.x_axis,
|
|
||||||
y_axis: params.y_axis,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (params.segment) data.segment = params.segment;
|
|
||||||
if (params.project) data.project = params.project;
|
|
||||||
|
|
||||||
analyticsService
|
|
||||||
.exportAnalytics(workspaceSlug.toString(), data)
|
|
||||||
.then((res) => {
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.SUCCESS,
|
|
||||||
title: "Success!",
|
|
||||||
message: res.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
trackExportAnalytics();
|
|
||||||
})
|
|
||||||
.catch(() =>
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.ERROR,
|
|
||||||
title: "Error!",
|
|
||||||
message: "There was some error in exporting the analytics. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
|
||||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
|
||||||
|
|
||||||
// fetch cycle details
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return;
|
|
||||||
|
|
||||||
fetchCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
|
||||||
}, [cycleId, cycleDetails, fetchCycleDetails, projectId, workspaceSlug]);
|
|
||||||
|
|
||||||
// fetch module details
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return;
|
|
||||||
|
|
||||||
fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
|
|
||||||
}, [moduleId, moduleDetails, fetchModuleDetails, projectId, workspaceSlug]);
|
|
||||||
|
|
||||||
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-full w-full items-start justify-between gap-2 bg-custom-sidebar-background-100 px-5 py-4",
|
|
||||||
!isProjectLevel ? "flex-col" : ""
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
|
||||||
<LayersIcon height={14} width={14} />
|
|
||||||
{analytics ? analytics.total : "..."}
|
|
||||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>{t("work_items")}</div>
|
|
||||||
</div>
|
|
||||||
{isProjectLevel && (
|
|
||||||
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
{renderFormattedDate(
|
|
||||||
(cycleId
|
|
||||||
? cycleDetails?.created_at
|
|
||||||
: moduleId
|
|
||||||
? moduleDetails?.created_at
|
|
||||||
: projectDetails?.created_at) ?? ""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cn("h-full w-full overflow-hidden", isProjectLevel ? "hidden" : "block")}>
|
|
||||||
<>
|
|
||||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
|
||||||
<CustomAnalyticsSidebarProjectsList
|
|
||||||
projectIds={selectedProjects}
|
|
||||||
isLoading={isProjectAnalyticsLoading}
|
|
||||||
isUpdating={isProjectAnalyticsUpdating}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CustomAnalyticsSidebarHeader />
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="neutral-primary"
|
|
||||||
prependIcon={<RefreshCw className="h-3 w-3 md:h-3.5 md:w-3.5" />}
|
|
||||||
onClick={() => {
|
|
||||||
if (!workspaceSlug) return;
|
|
||||||
|
|
||||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={cn(isProjectLevel ? "hidden md:block" : "", "capitalize")}>{t("refresh")}</div>
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
|
|
||||||
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>{t("exporter.csv.short_description")}</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BarDatum } from "@nivo/bar";
|
|
||||||
// plane package imports
|
|
||||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "@plane/constants";
|
|
||||||
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "@plane/types";
|
|
||||||
import { PriorityIcon, Tooltip } from "@plane/ui";
|
|
||||||
// helpers
|
|
||||||
import { generateBarColor, generateDisplayName, renderChartDynamicLabel } from "@/helpers/analytics.helper";
|
|
||||||
import { cn } from "@/helpers/common.helper";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
analytics: IAnalyticsResponse;
|
|
||||||
barGraphData: {
|
|
||||||
data: BarDatum[];
|
|
||||||
xAxisKeys: string[];
|
|
||||||
};
|
|
||||||
params: IAnalyticsParams;
|
|
||||||
yAxisKey: "count" | "estimate";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => (
|
|
||||||
<div className="w-full overflow-hidden overflow-x-auto">
|
|
||||||
<table className="w-full overflow-hidden divide-y divide-custom-border-200 whitespace-nowrap border-y border-custom-border-200">
|
|
||||||
<thead className="bg-custom-background-80">
|
|
||||||
<tr className="divide-x divide-custom-border-200 text-sm text-custom-text-100">
|
|
||||||
<th scope="col" className="px-page-x py-3 text-left font-medium">
|
|
||||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
|
||||||
</th>
|
|
||||||
{params.segment ? (
|
|
||||||
barGraphData.xAxisKeys.map((key) => (
|
|
||||||
<th
|
|
||||||
key={`segment-${key}`}
|
|
||||||
scope="col"
|
|
||||||
className={`px-page-x py-3 text-left font-medium ${
|
|
||||||
params.segment === "priority" || params.segment === "state__group" ? "capitalize" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{params.segment === "priority" ? (
|
|
||||||
<PriorityIcon priority={key as TIssuePriorities} />
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className="h-3 w-3 flex-shrink-0 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: generateBarColor(key, analytics, params, "segment"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{renderChartDynamicLabel(generateDisplayName(key, analytics, params, "segment"))?.label}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<th scope="col" className="px-page-x py-3 text-left font-medium sm:pr-0">
|
|
||||||
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label}
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-custom-border-200">
|
|
||||||
{barGraphData.data.map((item, index) => (
|
|
||||||
<tr key={`table-row-${index}`} className="divide-x divide-custom-border-200 text-xs text-custom-text-200">
|
|
||||||
<td className="px-page-x py-2">
|
|
||||||
<div className="relative flex items-center gap-2 w-full overflow-hidden">
|
|
||||||
<div className="flex-shrink-0 h-3 w-3 rounded overflow-hidden">
|
|
||||||
{params.x_axis === "priority" ? (
|
|
||||||
<PriorityIcon size={12} priority={(item.name as string).toLowerCase() as TIssuePriorities} />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="w-full h-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: generateBarColor(`${item.name}`, analytics, params, "x_axis"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"font-medium",
|
|
||||||
["priority", "state__group"].includes(params.x_axis) ? `capitalize` : ``
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Tooltip tooltipContent={generateDisplayName(`${item.name}`, analytics, params, "x_axis")}>
|
|
||||||
<div className="overflow-hidden w-full whitespace-normal break-words truncate line-clamp-1">
|
|
||||||
{generateDisplayName(`${item.name}`, analytics, params, "x_axis")}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{params.segment ? (
|
|
||||||
barGraphData.xAxisKeys.map((key, index) => (
|
|
||||||
<td key={`segment-value-${index}`} className="whitespace-nowrap px-page-x py-2 sm:pr-0">
|
|
||||||
{item[key] ?? 0}
|
|
||||||
</td>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<td className="whitespace-nowrap px-page-x py-2 sm:pr-0">{item[yAxisKey]}</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
@ -11,8 +11,8 @@ type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnalyticsV2EmptyState = ({ title, description, assetPath, className }: Props) => {
|
const AnalyticsEmptyState = ({ title, description, assetPath, className }: Props) => {
|
||||||
const backgroundReolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-grid-background" });
|
const backgroundReolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-grid-background" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -45,4 +45,4 @@ const AnalyticsV2EmptyState = ({ title, description, assetPath, className }: Pro
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default AnalyticsV2EmptyState;
|
export default AnalyticsEmptyState;
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
export * from "./custom-analytics";
|
export * from "./overview/root";
|
||||||
export * from "./scope-and-demand";
|
|
||||||
export * from "./project-modal";
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// plane package imports
|
// plane package imports
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { IAnalyticsResponseFieldsV2 } from "@plane/types";
|
import { IAnalyticsResponseFields } from "@plane/types";
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import TrendPiece from "./trend-piece";
|
import TrendPiece from "./trend-piece";
|
||||||
|
|
||||||
export type InsightCardProps = {
|
export type InsightCardProps = {
|
||||||
data?: IAnalyticsResponseFieldsV2;
|
data?: IAnalyticsResponseFields;
|
||||||
label: string;
|
label: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
versus?: string | null;
|
versus?: string | null;
|
||||||
|
|
@ -23,7 +23,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||||
import AnalyticsV2EmptyState from "../empty-state";
|
import AnalyticsEmptyState from "../empty-state";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
|
@ -40,7 +40,7 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
|
||||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-table" });
|
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-table" });
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
|
|
@ -155,9 +155,9 @@ export function DataTable<TData, TValue>({ columns, data, searchPlaceholder, act
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="p-0">
|
<TableCell colSpan={columns.length} className="p-0">
|
||||||
<div className="flex h-[350px] w-full items-center justify-center border border-custom-border-100 ">
|
<div className="flex h-[350px] w-full items-center justify-center border border-custom-border-100 ">
|
||||||
<AnalyticsV2EmptyState
|
<AnalyticsEmptyState
|
||||||
title={t("workspace_analytics.empty_state_v2.customized_insights.title")}
|
title={t("workspace_analytics.empty_state.customized_insights.title")}
|
||||||
description={t("workspace_analytics.empty_state_v2.customized_insights.description")}
|
description={t("workspace_analytics.empty_state.customized_insights.description")}
|
||||||
className="border-0"
|
className="border-0"
|
||||||
assetPath={resolvedPath}
|
assetPath={resolvedPath}
|
||||||
/>
|
/>
|
||||||
49
web/core/components/analytics/insight-table/root.tsx
Normal file
49
web/core/components/analytics/insight-table/root.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { ColumnDef, Row, Table } from "@tanstack/react-table";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { AnalyticsTableDataMap, TAnalyticsTabsBase } from "@plane/types";
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
import { DataTable } from "./data-table";
|
||||||
|
import { TableLoader } from "./loader";
|
||||||
|
interface InsightTableProps<T extends Exclude<TAnalyticsTabsBase, "overview">> {
|
||||||
|
analyticsType: T;
|
||||||
|
data?: AnalyticsTableDataMap[T][];
|
||||||
|
isLoading?: boolean;
|
||||||
|
columns: ColumnDef<AnalyticsTableDataMap[T]>[];
|
||||||
|
columnsLabels?: Record<string, string>;
|
||||||
|
headerText: string;
|
||||||
|
onExport?: (rows: Row<AnalyticsTableDataMap[T]>[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InsightTable = <T extends Exclude<TAnalyticsTabsBase, "overview">>(
|
||||||
|
props: InsightTableProps<T>
|
||||||
|
): React.ReactElement => {
|
||||||
|
const { data, isLoading, columns, headerText, onExport } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (isLoading) {
|
||||||
|
return <TableLoader columns={columns} rows={5} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
{data ? (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
searchPlaceholder={`${data.length} ${headerText}`}
|
||||||
|
actions={(table: Table<AnalyticsTableDataMap[T]>) => (
|
||||||
|
<Button
|
||||||
|
variant="accent-primary"
|
||||||
|
prependIcon={<Download className="h-3.5 w-3.5" />}
|
||||||
|
onClick={() => onExport?.(table.getFilteredRowModel().rows)}
|
||||||
|
>
|
||||||
|
<div>{t("exporter.csv.short_description")}</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>{t("common.no_data_yet")}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { Fragment } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
// plane package imports
|
|
||||||
import { ANALYTICS_TABS, EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { Header, EHeaderVariant } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
|
|
||||||
import { PageHead } from "@/components/core";
|
|
||||||
import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state";
|
|
||||||
// hooks
|
|
||||||
import { useCommandPalette, useEventTracker, useProject, useUserPermissions, useWorkspace } from "@/hooks/store";
|
|
||||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
|
||||||
|
|
||||||
const OldAnalyticsPage = observer(() => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const analytics_tab = searchParams.get("analytics_tab");
|
|
||||||
// plane imports
|
|
||||||
const { t } = useTranslation();
|
|
||||||
// store hooks
|
|
||||||
const { toggleCreateProjectModal } = useCommandPalette();
|
|
||||||
const { setTrackElement } = useEventTracker();
|
|
||||||
const { workspaceProjectIds, loader } = useProject();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { allowPermissions } = useUserPermissions();
|
|
||||||
// helper hooks
|
|
||||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" });
|
|
||||||
// derived values
|
|
||||||
const pageTitle = currentWorkspace?.name
|
|
||||||
? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name })
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// permissions
|
|
||||||
const canPerformEmptyStateActions = allowPermissions(
|
|
||||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
|
||||||
EUserPermissionsLevel.WORKSPACE
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: refactor loader implementation
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageHead title={pageTitle} />
|
|
||||||
{workspaceProjectIds && (
|
|
||||||
<>
|
|
||||||
{workspaceProjectIds.length > 0 || loader === "init-loader" ? (
|
|
||||||
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
|
|
||||||
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
|
||||||
<Header variant={EHeaderVariant.SECONDARY}>
|
|
||||||
<Tab.List as="div" className="flex space-x-2 h-full">
|
|
||||||
{ANALYTICS_TABS.map((tab) => (
|
|
||||||
<Tab key={tab.key} as={Fragment}>
|
|
||||||
{({ selected }) => (
|
|
||||||
<button
|
|
||||||
className={`text-sm group relative flex items-center gap-1 h-full px-3 cursor-pointer transition-all font-medium outline-none ${
|
|
||||||
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t(tab.i18n_title)}
|
|
||||||
<div
|
|
||||||
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tab.List>
|
|
||||||
</Header>
|
|
||||||
<Tab.Panels as={Fragment}>
|
|
||||||
<Tab.Panel as={Fragment}>
|
|
||||||
<ScopeAndDemand fullScreen />
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel as={Fragment}>
|
|
||||||
<CustomAnalytics fullScreen />
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<DetailedEmptyState
|
|
||||||
title={t("workspace_analytics.empty_state.general.title")}
|
|
||||||
description={t("workspace_analytics.empty_state.general.description")}
|
|
||||||
assetPath={resolvedPath}
|
|
||||||
customPrimaryButton={
|
|
||||||
<ComicBoxButton
|
|
||||||
label={t("workspace_analytics.empty_state.general.primary_button.text")}
|
|
||||||
title={t("workspace_analytics.empty_state.general.primary_button.comic.title")}
|
|
||||||
description={t("workspace_analytics.empty_state.general.primary_button.comic.description")}
|
|
||||||
onClick={() => {
|
|
||||||
setTrackElement("Analytics empty state");
|
|
||||||
toggleCreateProjectModal(true);
|
|
||||||
}}
|
|
||||||
disabled={!canPerformEmptyStateActions}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default OldAnalyticsPage;
|
|
||||||
|
|
@ -6,7 +6,7 @@ import useSWR from "swr";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// plane web hooks
|
// plane web hooks
|
||||||
import { useAnalyticsV2, useProject } from "@/hooks/store";
|
import { useAnalytics, useProject } from "@/hooks/store";
|
||||||
// plane web components
|
// plane web components
|
||||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||||
import ActiveProjectItem from "./active-project-item";
|
import ActiveProjectItem from "./active-project-item";
|
||||||
|
|
@ -15,7 +15,7 @@ const ActiveProjects = observer(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { fetchProjectAnalyticsCount } = useProject();
|
const { fetchProjectAnalyticsCount } = useProject();
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
const { selectedDurationLabel } = useAnalyticsV2();
|
const { selectedDurationLabel } = useAnalytics();
|
||||||
const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR(
|
const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR(
|
||||||
workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null,
|
workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
|
|
@ -6,13 +6,13 @@ import useSWR from "swr";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { TChartData } from "@plane/types";
|
import { TChartData } from "@plane/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2";
|
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||||
// services
|
// services
|
||||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||||
import { AnalyticsV2Service } from "@/services/analytics-v2.service";
|
import { AnalyticsService } from "@/services/analytics.service";
|
||||||
// plane web components
|
// plane web components
|
||||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||||
import AnalyticsV2EmptyState from "../empty-state";
|
import AnalyticsEmptyState from "../empty-state";
|
||||||
import { ProjectInsightsLoader } from "../loaders";
|
import { ProjectInsightsLoader } from "../loaders";
|
||||||
|
|
||||||
const RadarChart = dynamic(() =>
|
const RadarChart = dynamic(() =>
|
||||||
|
|
@ -21,25 +21,28 @@ const RadarChart = dynamic(() =>
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const analyticsV2Service = new AnalyticsV2Service();
|
const analyticsService = new AnalyticsService();
|
||||||
|
|
||||||
const ProjectInsights = observer(() => {
|
const ProjectInsights = observer(() => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const workspaceSlug = params.workspaceSlug.toString();
|
const workspaceSlug = params.workspaceSlug.toString();
|
||||||
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
|
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
|
||||||
useAnalyticsV2();
|
useAnalytics();
|
||||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-radar" });
|
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" });
|
||||||
|
|
||||||
const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(
|
const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(
|
||||||
`radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
`radar-chart-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
||||||
() =>
|
() =>
|
||||||
analyticsV2Service.getAdvanceAnalyticsCharts<TChartData<string, string>[]>(workspaceSlug, "projects", {
|
analyticsService.getAdvanceAnalyticsCharts<TChartData<string, string>[]>(
|
||||||
// date_filter: selectedDuration,
|
workspaceSlug,
|
||||||
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
|
"projects",
|
||||||
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
|
{
|
||||||
...(selectedModule ? { module_id: selectedModule } : {}),
|
// date_filter: selectedDuration,
|
||||||
},
|
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
|
||||||
|
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
|
||||||
|
...(selectedModule ? { module_id: selectedModule } : {}),
|
||||||
|
},
|
||||||
isPeekView
|
isPeekView
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -53,9 +56,9 @@ const ProjectInsights = observer(() => {
|
||||||
{isLoadingProjectInsight ? (
|
{isLoadingProjectInsight ? (
|
||||||
<ProjectInsightsLoader />
|
<ProjectInsightsLoader />
|
||||||
) : projectInsightsData && projectInsightsData?.length == 0 ? (
|
) : projectInsightsData && projectInsightsData?.length == 0 ? (
|
||||||
<AnalyticsV2EmptyState
|
<AnalyticsEmptyState
|
||||||
title={t("workspace_analytics.empty_state_v2.project_insights.title")}
|
title={t("workspace_analytics.empty_state.project_insights.title")}
|
||||||
description={t("workspace_analytics.empty_state_v2.project_insights.description")}
|
description={t("workspace_analytics.empty_state.project_insights.description")}
|
||||||
className="h-[300px]"
|
className="h-[300px]"
|
||||||
assetPath={resolvedPath}
|
assetPath={resolvedPath}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { observer } from "mobx-react";
|
|
||||||
|
|
||||||
// icons
|
|
||||||
import { Expand, Shrink, X } from "lucide-react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
fullScreen: boolean;
|
|
||||||
handleClose: () => void;
|
|
||||||
setFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectAnalyticsModalHeader: React.FC<Props> = observer((props) => {
|
|
||||||
const { fullScreen, handleClose, setFullScreen, title } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
|
|
||||||
<h3 className="break-words">Analytics for {title}</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hidden md:grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
onClick={() => setFullScreen((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
{fullScreen ? <Shrink size={14} strokeWidth={2} /> : <Expand size={14} strokeWidth={2} />}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid place-items-center p-1 text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
<X size={14} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./header";
|
|
||||||
export * from "./main-content";
|
|
||||||
export * from "./modal";
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import React, { Fragment } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
// plane package imports
|
|
||||||
import { ANALYTICS_TABS } from "@plane/constants";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { ICycle, IModule, IProject } from "@plane/types";
|
|
||||||
// components
|
|
||||||
import { CustomAnalytics, ScopeAndDemand } from "@/components/analytics";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
fullScreen: boolean;
|
|
||||||
cycleDetails: ICycle | undefined;
|
|
||||||
moduleDetails: IModule | undefined;
|
|
||||||
projectDetails: IProject | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props) => {
|
|
||||||
const { fullScreen, cycleDetails, moduleDetails } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<Tab.Group as={React.Fragment}>
|
|
||||||
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200 px-0 md:px-3">
|
|
||||||
{ANALYTICS_TABS.map((tab) => (
|
|
||||||
<Tab key={tab.key} as={Fragment}>
|
|
||||||
{({ selected }) => (
|
|
||||||
<button
|
|
||||||
className={`text-sm group relative flex items-center gap-1 h-[50px] px-3 cursor-pointer transition-all font-medium outline-none ${
|
|
||||||
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t(tab.i18n_title)}
|
|
||||||
<div
|
|
||||||
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tab.List>
|
|
||||||
<Tab.Panels as={React.Fragment}>
|
|
||||||
<Tab.Panel as={React.Fragment}>
|
|
||||||
<ScopeAndDemand fullScreen={fullScreen} />
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel as={React.Fragment}>
|
|
||||||
<CustomAnalytics
|
|
||||||
additionalParams={{
|
|
||||||
cycle: cycleDetails?.id,
|
|
||||||
module: moduleDetails?.id,
|
|
||||||
}}
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { ICycle, IModule, IProject } from "@plane/types";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { ProjectAnalyticsModalHeader, ProjectAnalyticsModalMainContent } from "@/components/analytics";
|
|
||||||
// types
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
cycleDetails?: ICycle | undefined;
|
|
||||||
moduleDetails?: IModule | undefined;
|
|
||||||
projectDetails?: IProject | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectAnalyticsModal: React.FC<Props> = observer((props) => {
|
|
||||||
const { isOpen, onClose, cycleDetails, moduleDetails, projectDetails } = props;
|
|
||||||
|
|
||||||
const [fullScreen, setFullScreen] = useState(false);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root appear show={isOpen} as={React.Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
|
||||||
<Transition.Child
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition-transform duration-300"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transition-transform duration-200"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
|
||||||
<Dialog.Panel>
|
|
||||||
<div
|
|
||||||
className={`fixed right-0 top-0 z-20 h-full bg-custom-background-100 shadow-custom-shadow-md ${
|
|
||||||
fullScreen ? "w-full p-2" : "w-full sm:w-full md:w-1/2"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
|
|
||||||
fullScreen ? "rounded-lg border" : "border-l"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<ProjectAnalyticsModalHeader
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
handleClose={handleClose}
|
|
||||||
setFullScreen={setFullScreen}
|
|
||||||
title={cycleDetails?.name ?? moduleDetails?.name ?? projectDetails?.name ?? ""}
|
|
||||||
/>
|
|
||||||
<ProjectAnalyticsModalMainContent
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
cycleDetails={cycleDetails}
|
|
||||||
moduleDetails={moduleDetails}
|
|
||||||
projectDetails={projectDetails}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
// plane imports
|
|
||||||
import { STATE_GROUPS } from "@plane/constants";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
// types
|
|
||||||
import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types";
|
|
||||||
// constants
|
|
||||||
import { Card } from "@plane/ui";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
defaultAnalytics: IDefaultAnalyticsResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-base font-medium text-custom-text-100">{t("workspace_analytics.open_tasks")}</h4>
|
|
||||||
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-6 pb-2">
|
|
||||||
{defaultAnalytics?.open_issues_classified.map((group) => {
|
|
||||||
const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={group.state_group} className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span
|
|
||||||
className="h-2 w-2 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: STATE_GROUPS[group.state_group as TStateGroups].color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<h6 className="capitalize">{group.state_group}</h6>
|
|
||||||
<span className="ml-1 rounded-3xl bg-custom-background-80 px-2 py-0.5 text-[0.65rem] text-custom-text-200">
|
|
||||||
{group.state_count}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-custom-text-200">{percentage}%</p>
|
|
||||||
</div>
|
|
||||||
<div className="bar relative h-1 w-full rounded bg-custom-background-80">
|
|
||||||
<div
|
|
||||||
className="absolute left-0 top-0 h-1 rounded duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${percentage}%`,
|
|
||||||
backgroundColor: STATE_GROUPS[group.state_group as TStateGroups].color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export * from "./demand";
|
|
||||||
export * from "./leaderboard";
|
|
||||||
export * from "./scope-and-demand";
|
|
||||||
export * from "./scope";
|
|
||||||
export * from "./year-wise-issues";
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
// plane ui
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { Card } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { ProfileEmptyState } from "@/components/ui";
|
|
||||||
// helpers
|
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
|
||||||
// image
|
|
||||||
import emptyUsers from "@/public/empty-state/empty_users.svg";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
users: {
|
|
||||||
avatar_url: string | null;
|
|
||||||
display_name: string | null;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
count: number;
|
|
||||||
id: string;
|
|
||||||
}[];
|
|
||||||
title: string;
|
|
||||||
emptyStateMessage: string;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyStateMessage, workspaceSlug }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<h6 className="text-base font-medium">{title}</h6>
|
|
||||||
{users.length > 0 ? (
|
|
||||||
<div className="mt-3 space-y-3">
|
|
||||||
{users.map((user) => (
|
|
||||||
<a
|
|
||||||
key={user?.display_name ?? "None"}
|
|
||||||
href={`/${workspaceSlug}/profile/${user.id}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-start justify-between gap-4 text-xs"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{user.avatar_url && user.avatar_url !== "" ? (
|
|
||||||
<div className="relative h-4 w-4 flex-shrink-0 rounded-full">
|
|
||||||
<img
|
|
||||||
src={getFileURL(user.avatar_url)}
|
|
||||||
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
|
|
||||||
alt={user?.display_name ?? "None"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid h-4 w-4 flex-shrink-0 place-items-center rounded-full bg-gray-700 text-[11px] capitalize text-white">
|
|
||||||
{user?.display_name !== "" ? user?.display_name?.[0] : "?"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="break-words text-custom-text-200">
|
|
||||||
{user?.display_name !== "" ? `${user?.display_name}` : "No assignee"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="flex-shrink-0">{user.count}</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="px-7 py-4">
|
|
||||||
<ProfileEmptyState title={t("no_data_yet")} description={emptyStateMessage} image={emptyUsers} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import useSWR from "swr";
|
|
||||||
// ui
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { Button, ContentWrapper, Loader } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { AnalyticsDemand, AnalyticsLeaderBoard, AnalyticsScope, AnalyticsYearWiseIssues } from "@/components/analytics";
|
|
||||||
// fetch-keys
|
|
||||||
import { DEFAULT_ANALYTICS } from "@/constants/fetch-keys";
|
|
||||||
// services
|
|
||||||
import { AnalyticsService } from "@/services/analytics.service";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
fullScreen?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const analyticsService = new AnalyticsService();
|
|
||||||
|
|
||||||
export const ScopeAndDemand: React.FC<Props> = (props) => {
|
|
||||||
const { fullScreen = true } = props;
|
|
||||||
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = useParams();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const isProjectLevel = projectId ? true : false;
|
|
||||||
|
|
||||||
const params = isProjectLevel
|
|
||||||
? {
|
|
||||||
project: projectId ? [projectId.toString()] : null,
|
|
||||||
cycle: cycleId ? cycleId.toString() : null,
|
|
||||||
module: moduleId ? moduleId.toString() : null,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: defaultAnalytics,
|
|
||||||
error: defaultAnalyticsError,
|
|
||||||
mutate: mutateDefaultAnalytics,
|
|
||||||
} = useSWR(
|
|
||||||
workspaceSlug ? DEFAULT_ANALYTICS(workspaceSlug.toString(), params) : null,
|
|
||||||
workspaceSlug ? () => analyticsService.getDefaultAnalytics(workspaceSlug.toString(), params) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
// scope data
|
|
||||||
const pendingIssues = defaultAnalytics?.pending_issue_user ?? [];
|
|
||||||
const pendingUnAssignedIssuesUser = pendingIssues?.find((issue) => issue.assignees__id === null);
|
|
||||||
const pendingAssignedIssues = pendingIssues?.filter((issue) => issue.assignees__id !== null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!defaultAnalyticsError ? (
|
|
||||||
defaultAnalytics ? (
|
|
||||||
<ContentWrapper>
|
|
||||||
<div className={`grid grid-cols-1 gap-5 ${fullScreen ? "md:grid-cols-2" : ""}`}>
|
|
||||||
<AnalyticsDemand defaultAnalytics={defaultAnalytics} />
|
|
||||||
<AnalyticsScope
|
|
||||||
pendingUnAssignedIssuesUser={pendingUnAssignedIssuesUser}
|
|
||||||
pendingAssignedIssues={pendingAssignedIssues}
|
|
||||||
/>
|
|
||||||
<AnalyticsLeaderBoard
|
|
||||||
users={defaultAnalytics.most_issue_created_user?.map((user) => ({
|
|
||||||
avatar_url: user?.created_by__avatar_url,
|
|
||||||
firstName: user?.created_by__first_name,
|
|
||||||
lastName: user?.created_by__last_name,
|
|
||||||
display_name: user?.created_by__display_name,
|
|
||||||
count: user?.count,
|
|
||||||
id: user?.created_by__id,
|
|
||||||
}))}
|
|
||||||
title={t("workspace_analytics.most_work_items_created.title")}
|
|
||||||
emptyStateMessage={t("workspace_analytics.most_work_items_created.empty_state")}
|
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
|
||||||
/>
|
|
||||||
<AnalyticsLeaderBoard
|
|
||||||
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
|
||||||
avatar_url: user?.assignees__avatar_url,
|
|
||||||
firstName: user?.assignees__first_name,
|
|
||||||
lastName: user?.assignees__last_name,
|
|
||||||
display_name: user?.assignees__display_name,
|
|
||||||
count: user?.count,
|
|
||||||
id: user?.assignees__id,
|
|
||||||
}))}
|
|
||||||
title={t("workspace_analytics.most_work_items_closed.title")}
|
|
||||||
emptyStateMessage={t("workspace_analytics.most_work_items_closed.empty_state")}
|
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
|
||||||
/>
|
|
||||||
<div className={fullScreen ? "md:col-span-2" : ""}>
|
|
||||||
<AnalyticsYearWiseIssues defaultAnalytics={defaultAnalytics} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ContentWrapper>
|
|
||||||
) : (
|
|
||||||
<Loader className="grid grid-cols-1 gap-5 p-5 lg:grid-cols-2">
|
|
||||||
<Loader.Item height="250px" />
|
|
||||||
<Loader.Item height="250px" />
|
|
||||||
<Loader.Item height="250px" />
|
|
||||||
<Loader.Item height="250px" />
|
|
||||||
</Loader>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="grid h-full place-items-center p-5">
|
|
||||||
<div className="space-y-4 text-custom-text-200">
|
|
||||||
<p className="text-sm">{t("workspace_analytics.error")}</p>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<Button variant="primary" onClick={() => mutateDefaultAnalytics()}>
|
|
||||||
{t("refresh")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
// plane types
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { IDefaultAnalyticsUser } from "@plane/types";
|
|
||||||
// plane ui
|
|
||||||
import { Card } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { BarGraph, ProfileEmptyState } from "@/components/ui";
|
|
||||||
// helpers
|
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
|
||||||
// image
|
|
||||||
import emptyBarGraph from "@/public/empty-state/empty_bar_graph.svg";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pendingUnAssignedIssuesUser: IDefaultAnalyticsUser | undefined;
|
|
||||||
pendingAssignedIssues: IDefaultAnalyticsUser[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalyticsScope: React.FC<Props> = ({ pendingUnAssignedIssuesUser, pendingAssignedIssues }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<div className="divide-y divide-custom-border-200">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h6 className="text-base font-medium">{t("workspace_analytics.pending_work_items.title")}</h6>
|
|
||||||
{pendingUnAssignedIssuesUser && (
|
|
||||||
<div className="relative flex items-center py-1 px-3 rounded-md gap-2 text-xs text-custom-primary-100 bg-custom-primary-100/10">
|
|
||||||
{t("unassigned")}: {pendingUnAssignedIssuesUser.count}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{pendingAssignedIssues && pendingAssignedIssues.length > 0 ? (
|
|
||||||
<BarGraph
|
|
||||||
data={pendingAssignedIssues}
|
|
||||||
indexBy="assignees__id"
|
|
||||||
keys={["count"]}
|
|
||||||
height="250px"
|
|
||||||
colors={() => `#f97316`}
|
|
||||||
customYAxisTickValues={pendingAssignedIssues.map((d) => (d.count > 0 ? d.count : 50))}
|
|
||||||
tooltip={(datum) => {
|
|
||||||
const assignee = pendingAssignedIssues.find((a) => a.assignees__id === `${datum.indexValue}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
|
||||||
<span className="font-medium text-custom-text-200">
|
|
||||||
{assignee ? assignee.assignees__display_name : "No assignee"}:{" "}
|
|
||||||
</span>
|
|
||||||
{datum.value}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
axisBottom={{
|
|
||||||
renderTick: (datum) => {
|
|
||||||
const assignee = pendingAssignedIssues[datum.tickIndex] ?? "";
|
|
||||||
|
|
||||||
if (assignee && assignee?.assignees__avatar_url && assignee?.assignees__avatar_url !== "")
|
|
||||||
return (
|
|
||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
|
||||||
<image
|
|
||||||
x={-8}
|
|
||||||
y={10}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
xlinkHref={getFileURL(assignee?.assignees__avatar_url)}
|
|
||||||
style={{ clipPath: "circle(50%)" }}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
else
|
|
||||||
return (
|
|
||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
|
||||||
<circle cy={18} r={8} fill="#374151" />
|
|
||||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
|
||||||
{datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
margin={{ top: 20 }}
|
|
||||||
theme={{
|
|
||||||
axis: {},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="px-7 py-4">
|
|
||||||
<ProfileEmptyState
|
|
||||||
title={t("no_data_yet")}
|
|
||||||
description={t("workspace_analytics.pending_work_items.empty_state")}
|
|
||||||
image={emptyBarGraph}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
// ui
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { IDefaultAnalyticsResponse } from "@plane/types";
|
|
||||||
import { Card } from "@plane/ui";
|
|
||||||
import { LineGraph, ProfileEmptyState } from "@/components/ui";
|
|
||||||
// image
|
|
||||||
import { MONTHS_LIST } from "@/constants/calendar";
|
|
||||||
import emptyGraph from "@/public/empty-state/empty_graph.svg";
|
|
||||||
// types
|
|
||||||
// constants
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
defaultAnalytics: IDefaultAnalyticsResponse;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<h1 className="py-3 text-base font-medium">{t("workspace_analytics.work_items_closed_in_a_year.title")}</h1>
|
|
||||||
{defaultAnalytics.issue_completed_month_wise.length > 0 ? (
|
|
||||||
<LineGraph
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
id: "issues_closed",
|
|
||||||
color: "rgb(var(--color-primary-100))",
|
|
||||||
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
|
|
||||||
x: t(month.shortTitle),
|
|
||||||
y:
|
|
||||||
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))
|
|
||||||
?.count || 0,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
|
|
||||||
height="300px"
|
|
||||||
colors={(datum) => datum.color}
|
|
||||||
curve="monotoneX"
|
|
||||||
margin={{ top: 20 }}
|
|
||||||
enableSlices="x"
|
|
||||||
sliceTooltip={(datum) => (
|
|
||||||
<div className="rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
|
||||||
{datum.slice.points[0].data.yFormatted}
|
|
||||||
<span className="text-custom-text-200"> {t("workspace_analytics.work_items_closed_in")} </span>
|
|
||||||
{datum.slice.points[0].data.xFormatted}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
theme={{
|
|
||||||
background: "rgb(var(--color-background-100))",
|
|
||||||
}}
|
|
||||||
enableArea
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="px-7 py-4">
|
|
||||||
<ProfileEmptyState
|
|
||||||
title={t("no_data_yet")}
|
|
||||||
description={t("workspace_analytics.work_items_closed_in_a_year.empty_state")}
|
|
||||||
image={emptyGraph}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -3,31 +3,30 @@ import { observer } from "mobx-react";
|
||||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
||||||
import { Calendar, SlidersHorizontal } from "lucide-react";
|
import { Calendar, SlidersHorizontal } from "lucide-react";
|
||||||
// plane package imports
|
// plane package imports
|
||||||
import { ANALYTICS_V2_X_AXIS_VALUES, ANALYTICS_V2_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants";
|
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, ChartYAxisMetric } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { IAnalyticsV2Params } from "@plane/types";
|
import { IAnalyticsParams } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { AnalyticsV2Service } from "@/services/analytics-v2.service";
|
|
||||||
import { SelectXAxis } from "./select-x-axis";
|
import { SelectXAxis } from "./select-x-axis";
|
||||||
import { SelectYAxis } from "./select-y-axis";
|
import { SelectYAxis } from "./select-y-axis";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Control<IAnalyticsV2Params, unknown>;
|
control: Control<IAnalyticsParams, unknown>;
|
||||||
setValue: UseFormSetValue<IAnalyticsV2Params>;
|
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||||
params: IAnalyticsV2Params;
|
params: IAnalyticsParams;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
classNames?: string;
|
classNames?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnalyticsV2SelectParams: React.FC<Props> = observer((props) => {
|
export const AnalyticsSelectParams: React.FC<Props> = observer((props) => {
|
||||||
const { control, params, classNames } = props;
|
const { control, params, classNames } = props;
|
||||||
const xAxisOptions = useMemo(
|
const xAxisOptions = useMemo(
|
||||||
() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.group_by),
|
() => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.group_by),
|
||||||
[params.group_by]
|
[params.group_by]
|
||||||
);
|
);
|
||||||
const groupByOptions = useMemo(
|
const groupByOptions = useMemo(
|
||||||
() => ANALYTICS_V2_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis),
|
() => ANALYTICS_X_AXIS_VALUES.filter((option) => option.value !== params.x_axis),
|
||||||
[params.x_axis]
|
[params.x_axis]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -43,7 +42,7 @@ export const AnalyticsV2SelectParams: React.FC<Props> = observer((props) => {
|
||||||
onChange={(val: ChartYAxisMetric | null) => {
|
onChange={(val: ChartYAxisMetric | null) => {
|
||||||
onChange(val);
|
onChange(val);
|
||||||
}}
|
}}
|
||||||
options={ANALYTICS_V2_Y_AXIS_VALUES}
|
options={ANALYTICS_Y_AXIS_VALUES}
|
||||||
hiddenOptions={[ChartYAxisMetric.ESTIMATE_POINT_COUNT]}
|
hiddenOptions={[ChartYAxisMetric.ESTIMATE_POINT_COUNT]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { Calendar } from "lucide-react";
|
import { Calendar } from "lucide-react";
|
||||||
// plane package imports
|
// plane package imports
|
||||||
import { ANALYTICS_V2_DURATION_FILTER_OPTIONS } from "@plane/constants";
|
import { ANALYTICS_DURATION_FILTER_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { CustomSearchSelect } from "@plane/ui";
|
import { CustomSearchSelect } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
|
|
@ -10,7 +10,7 @@ import { TDropdownProps } from "@/components/dropdowns/types";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
onChange: (val: (typeof ANALYTICS_V2_DURATION_FILTER_OPTIONS)[number]["value"]) => void;
|
onChange: (val: (typeof ANALYTICS_DURATION_FILTER_OPTIONS)[number]["value"]) => void;
|
||||||
//optional
|
//optional
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
dropdownArrow?: boolean;
|
dropdownArrow?: boolean;
|
||||||
|
|
@ -23,7 +23,7 @@ type Props = TDropdownProps & {
|
||||||
function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) {
|
function DurationDropdown({ placeholder = "Duration", onChange, value }: Props) {
|
||||||
useTranslation();
|
useTranslation();
|
||||||
|
|
||||||
const options = ANALYTICS_V2_DURATION_FILTER_OPTIONS.map((option) => ({
|
const options = ANALYTICS_DURATION_FILTER_OPTIONS.map((option) => ({
|
||||||
value: option.value,
|
value: option.value,
|
||||||
query: option.name,
|
query: option.name,
|
||||||
content: (
|
content: (
|
||||||
|
|
@ -40,7 +40,7 @@ function DurationDropdown({ placeholder = "Duration", onChange, value }: Props)
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center gap-2 p-1 ">
|
<div className="flex items-center gap-2 p-1 ">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4" />
|
||||||
{value ? ANALYTICS_V2_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder}
|
{value ? ANALYTICS_DURATION_FILTER_OPTIONS.find((opt) => opt.value === value)?.name : placeholder}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
102
web/core/components/analytics/total-insights.tsx
Normal file
102
web/core/components/analytics/total-insights.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// plane package imports
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { IInsightField, insightsFields } from "@plane/constants";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { IAnalyticsResponse, TAnalyticsTabsBase } from "@plane/types";
|
||||||
|
//hooks
|
||||||
|
import { cn } from "@/helpers/common.helper";
|
||||||
|
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||||
|
//services
|
||||||
|
import { AnalyticsService } from "@/services/analytics.service";
|
||||||
|
// plane web components
|
||||||
|
import InsightCard from "./insight-card";
|
||||||
|
|
||||||
|
const analyticsService = new AnalyticsService();
|
||||||
|
|
||||||
|
const getInsightLabel = (
|
||||||
|
analyticsType: TAnalyticsTabsBase,
|
||||||
|
item: IInsightField,
|
||||||
|
isEpic: boolean | undefined,
|
||||||
|
t: (key: string, options?: any) => string
|
||||||
|
) => {
|
||||||
|
if (analyticsType === "work-items") {
|
||||||
|
return isEpic
|
||||||
|
? t(item.i18nKey, { entity: t("common.epics") })
|
||||||
|
: t(item.i18nKey, { entity: t("common.work_items") });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the base translation with entity
|
||||||
|
const baseTranslation = t(item.i18nKey, {
|
||||||
|
...item.i18nProps,
|
||||||
|
entity: item.i18nProps?.entity && t(item.i18nProps?.entity),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add prefix if available
|
||||||
|
const prefix = item.i18nProps?.prefix ? `${t(item.i18nProps.prefix)} ` : "";
|
||||||
|
|
||||||
|
// Add suffix if available
|
||||||
|
const suffix = item.i18nProps?.suffix ? ` ${t(item.i18nProps.suffix)}` : "";
|
||||||
|
|
||||||
|
// Combine prefix, base translation, and suffix
|
||||||
|
return `${prefix}${baseTranslation}${suffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TotalInsights: React.FC<{
|
||||||
|
analyticsType: TAnalyticsTabsBase;
|
||||||
|
peekView?: boolean;
|
||||||
|
}> = observer(({ analyticsType, peekView }) => {
|
||||||
|
const params = useParams();
|
||||||
|
const workspaceSlug = params.workspaceSlug.toString();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
selectedDuration,
|
||||||
|
selectedProjects,
|
||||||
|
selectedDurationLabel,
|
||||||
|
selectedCycle,
|
||||||
|
selectedModule,
|
||||||
|
isPeekView,
|
||||||
|
isEpic,
|
||||||
|
} = useAnalytics();
|
||||||
|
const { data: totalInsightsData, isLoading } = useSWR(
|
||||||
|
`total-insights-${analyticsType}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isEpic}`,
|
||||||
|
() =>
|
||||||
|
analyticsService.getAdvanceAnalytics<IAnalyticsResponse>(
|
||||||
|
workspaceSlug,
|
||||||
|
analyticsType,
|
||||||
|
{
|
||||||
|
// date_filter: selectedDuration,
|
||||||
|
...(selectedProjects?.length > 0 ? { project_ids: selectedProjects.join(",") } : {}),
|
||||||
|
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
|
||||||
|
...(selectedModule ? { module_id: selectedModule } : {}),
|
||||||
|
...(isEpic ? { epic: true } : {}),
|
||||||
|
},
|
||||||
|
isPeekView
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid grid-cols-1 gap-8 sm:grid-cols-2 md:gap-10",
|
||||||
|
!peekView
|
||||||
|
? insightsFields[analyticsType]?.length % 5 === 0
|
||||||
|
? "gap-10 lg:grid-cols-5"
|
||||||
|
: "gap-8 lg:grid-cols-4"
|
||||||
|
: "grid-cols-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{insightsFields[analyticsType]?.map((item) => (
|
||||||
|
<InsightCard
|
||||||
|
key={`${analyticsType}-${item.key}`}
|
||||||
|
isLoading={isLoading}
|
||||||
|
data={totalInsightsData?.[item.key]}
|
||||||
|
label={getInsightLabel(analyticsType, item, isEpic, t)}
|
||||||
|
versus={selectedDurationLabel}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TotalInsights;
|
||||||
|
|
@ -5,35 +5,46 @@ import useSWR from "swr";
|
||||||
// plane package imports
|
// plane package imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { AreaChart } from "@plane/propel/charts/area-chart";
|
import { AreaChart } from "@plane/propel/charts/area-chart";
|
||||||
import { IChartResponseV2, TChartData } from "@plane/types";
|
import { IChartResponse, TChartData } from "@plane/types";
|
||||||
import { renderFormattedDate } from "@plane/utils";
|
import { renderFormattedDate } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAnalyticsV2 } from "@/hooks/store/use-analytics-v2";
|
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||||
// services
|
// services
|
||||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||||
import { AnalyticsV2Service } from "@/services/analytics-v2.service";
|
import { AnalyticsService } from "@/services/analytics.service";
|
||||||
// plane web components
|
// plane web components
|
||||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||||
import AnalyticsV2EmptyState from "../empty-state";
|
import AnalyticsEmptyState from "../empty-state";
|
||||||
import { ChartLoader } from "../loaders";
|
import { ChartLoader } from "../loaders";
|
||||||
|
|
||||||
const analyticsV2Service = new AnalyticsV2Service();
|
const analyticsService = new AnalyticsService();
|
||||||
const CreatedVsResolved = observer(() => {
|
const CreatedVsResolved = observer(() => {
|
||||||
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
|
const {
|
||||||
useAnalyticsV2();
|
selectedDuration,
|
||||||
|
selectedDurationLabel,
|
||||||
|
selectedProjects,
|
||||||
|
selectedCycle,
|
||||||
|
selectedModule,
|
||||||
|
isPeekView,
|
||||||
|
isEpic,
|
||||||
|
} = useAnalytics();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const workspaceSlug = params.workspaceSlug.toString();
|
const workspaceSlug = params.workspaceSlug.toString();
|
||||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics-v2/empty-chart-area" });
|
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-area" });
|
||||||
const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR(
|
const { data: createdVsResolvedData, isLoading: isCreatedVsResolvedLoading } = useSWR(
|
||||||
`created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
`created-vs-resolved-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}-${isEpic}`,
|
||||||
() =>
|
() =>
|
||||||
analyticsV2Service.getAdvanceAnalyticsCharts<IChartResponseV2>(workspaceSlug, "work-items", {
|
analyticsService.getAdvanceAnalyticsCharts<IChartResponse>(
|
||||||
// date_filter: selectedDuration,
|
workspaceSlug,
|
||||||
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
|
"work-items",
|
||||||
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
|
{
|
||||||
...(selectedModule ? { module_id: selectedModule } : {}),
|
// date_filter: selectedDuration,
|
||||||
},
|
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
|
||||||
|
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
|
||||||
|
...(selectedModule ? { module_id: selectedModule } : {}),
|
||||||
|
...(isEpic ? { epic: true } : {}),
|
||||||
|
},
|
||||||
isPeekView
|
isPeekView
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -89,11 +100,11 @@ const CreatedVsResolved = observer(() => {
|
||||||
areas={areas}
|
areas={areas}
|
||||||
xAxis={{
|
xAxis={{
|
||||||
key: "name",
|
key: "name",
|
||||||
label: "Date",
|
label: t("date"),
|
||||||
}}
|
}}
|
||||||
yAxis={{
|
yAxis={{
|
||||||
key: "count",
|
key: "count",
|
||||||
label: "Number of Issues",
|
label: t("no_of", { entity: t("work_items") }),
|
||||||
offset: -30,
|
offset: -30,
|
||||||
dx: -22,
|
dx: -22,
|
||||||
}}
|
}}
|
||||||
|
|
@ -110,9 +121,9 @@ const CreatedVsResolved = observer(() => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AnalyticsV2EmptyState
|
<AnalyticsEmptyState
|
||||||
title={t("workspace_analytics.empty_state_v2.created_vs_resolved.title")}
|
title={t("workspace_analytics.empty_state.created_vs_resolved.title")}
|
||||||
description={t("workspace_analytics.empty_state_v2.created_vs_resolved.description")}
|
description={t("workspace_analytics.empty_state.created_vs_resolved.description")}
|
||||||
className="h-[350px]"
|
className="h-[350px]"
|
||||||
assetPath={resolvedPath}
|
assetPath={resolvedPath}
|
||||||
/>
|
/>
|
||||||
|
|
@ -4,14 +4,14 @@ import { useForm } from "react-hook-form";
|
||||||
// plane package imports
|
// plane package imports
|
||||||
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants";
|
import { ChartXAxisProperty, ChartYAxisMetric } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { IAnalyticsV2Params } from "@plane/types";
|
import { IAnalyticsParams } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// plane web components
|
// plane web components
|
||||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||||
import { AnalyticsV2SelectParams } from "../select/analytics-params";
|
import { AnalyticsSelectParams } from "../select/analytics-params";
|
||||||
import PriorityChart from "./priority-chart";
|
import PriorityChart from "./priority-chart";
|
||||||
|
|
||||||
const defaultValues: IAnalyticsV2Params = {
|
const defaultValues: IAnalyticsParams = {
|
||||||
x_axis: ChartXAxisProperty.PRIORITY,
|
x_axis: ChartXAxisProperty.PRIORITY,
|
||||||
y_axis: ChartYAxisMetric.WORK_ITEM_COUNT,
|
y_axis: ChartYAxisMetric.WORK_ITEM_COUNT,
|
||||||
};
|
};
|
||||||
|
|
@ -19,7 +19,7 @@ const defaultValues: IAnalyticsV2Params = {
|
||||||
const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => {
|
const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
const { control, watch, setValue } = useForm<IAnalyticsV2Params>({
|
const { control, watch, setValue } = useForm<IAnalyticsParams>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
},
|
},
|
||||||
|
|
@ -37,7 +37,7 @@ const CustomizedInsights = observer(({ peekView }: { peekView?: boolean }) => {
|
||||||
className="col-span-1"
|
className="col-span-1"
|
||||||
headerClassName={cn(peekView ? "flex-col items-start" : "")}
|
headerClassName={cn(peekView ? "flex-col items-start" : "")}
|
||||||
actions={
|
actions={
|
||||||
<AnalyticsV2SelectParams
|
<AnalyticsSelectParams
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
params={params}
|
params={params}
|
||||||
|
|
@ -5,7 +5,7 @@ import { Tab } from "@headlessui/react";
|
||||||
import { ICycle, IModule, IProject } from "@plane/types";
|
import { ICycle, IModule, IProject } from "@plane/types";
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useAnalyticsV2 } from "@/hooks/store";
|
import { useAnalytics } from "@/hooks/store";
|
||||||
// plane web components
|
// plane web components
|
||||||
import TotalInsights from "../../total-insights";
|
import TotalInsights from "../../total-insights";
|
||||||
import CreatedVsResolved from "../created-vs-resolved";
|
import CreatedVsResolved from "../created-vs-resolved";
|
||||||
|
|
@ -21,7 +21,7 @@ type Props = {
|
||||||
|
|
||||||
export const WorkItemsModalMainContent: React.FC<Props> = observer((props) => {
|
export const WorkItemsModalMainContent: React.FC<Props> = observer((props) => {
|
||||||
const { projectDetails, cycleDetails, moduleDetails, fullScreen } = props;
|
const { projectDetails, cycleDetails, moduleDetails, fullScreen } = props;
|
||||||
const { updateSelectedProjects, updateSelectedCycle, updateSelectedModule, updateIsPeekView } = useAnalyticsV2();
|
const { updateSelectedProjects, updateSelectedCycle, updateSelectedModule, updateIsPeekView } = useAnalytics();
|
||||||
const [isModalConfigured, setIsModalConfigured] = useState(false);
|
const [isModalConfigured, setIsModalConfigured] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// plane package imports
|
// plane package imports
|
||||||
import { ICycle, IModule, IProject } from "@plane/types";
|
import { ICycle, IModule, IProject } from "@plane/types";
|
||||||
|
import { useAnalytics } from "@/hooks/store";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { WorkItemsModalMainContent } from "./content";
|
import { WorkItemsModalMainContent } from "./content";
|
||||||
import { WorkItemsModalHeader } from "./header";
|
import { WorkItemsModalHeader } from "./header";
|
||||||
|
|
@ -13,17 +14,22 @@ type Props = {
|
||||||
projectDetails?: IProject | undefined;
|
projectDetails?: IProject | undefined;
|
||||||
cycleDetails?: ICycle | undefined;
|
cycleDetails?: ICycle | undefined;
|
||||||
moduleDetails?: IModule | undefined;
|
moduleDetails?: IModule | undefined;
|
||||||
|
isEpic?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkItemsModal: React.FC<Props> = observer((props) => {
|
export const WorkItemsModal: React.FC<Props> = observer((props) => {
|
||||||
const { isOpen, onClose, projectDetails, moduleDetails, cycleDetails } = props;
|
const { isOpen, onClose, projectDetails, moduleDetails, cycleDetails, isEpic } = props;
|
||||||
|
const { updateIsEpic } = useAnalytics();
|
||||||
const [fullScreen, setFullScreen] = useState(false);
|
const [fullScreen, setFullScreen] = useState(false);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateIsEpic(isEpic ?? false);
|
||||||
|
}, [isEpic, updateIsEpic]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root appear show={isOpen} as={React.Fragment}>
|
<Transition.Root appear show={isOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue