feat: init
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { ProjectIcon } from "@plane/propel/icons";
|
||||
// plane package imports
|
||||
import { cn } from "@plane/utils";
|
||||
import { Logo } from "@/components/common/logo";
|
||||
// plane web hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
type Props = {
|
||||
project: {
|
||||
id: string;
|
||||
completed_issues?: number;
|
||||
total_issues?: number;
|
||||
};
|
||||
isLoading?: boolean;
|
||||
};
|
||||
const CompletionPercentage = ({ percentage }: { percentage: number }) => {
|
||||
const percentageColor = percentage > 50 ? "bg-green-500/30 text-green-500" : "bg-red-500/30 text-red-500";
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2 rounded p-1 text-xs", percentageColor)}>
|
||||
<span>{percentage}%</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ActiveProjectItem = (props: Props) => {
|
||||
const { project } = props;
|
||||
const { getProjectById } = useProject();
|
||||
const { id, completed_issues, total_issues } = project;
|
||||
|
||||
const projectDetails = getProjectById(id);
|
||||
|
||||
if (!projectDetails) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 ">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-custom-background-80">
|
||||
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
||||
{projectDetails?.logo_props ? (
|
||||
<Logo logo={projectDetails?.logo_props} size={16} />
|
||||
) : (
|
||||
<span className="grid h-4 w-4 flex-shrink-0 place-items-center">
|
||||
<ProjectIcon className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium">{projectDetails?.name}</p>
|
||||
</div>
|
||||
<CompletionPercentage
|
||||
percentage={completed_issues && total_issues ? Math.round((completed_issues / total_issues) * 100) : 0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveProjectItem;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane package imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Loader } from "@plane/ui";
|
||||
// plane web hooks
|
||||
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web components
|
||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||
import ActiveProjectItem from "./active-project-item";
|
||||
|
||||
const ActiveProjects = observer(() => {
|
||||
const { t } = useTranslation();
|
||||
const { fetchProjectAnalyticsCount } = useProject();
|
||||
const { workspaceSlug } = useParams();
|
||||
const { selectedDurationLabel } = useAnalytics();
|
||||
const { data: projectAnalyticsCount, isLoading: isProjectAnalyticsCountLoading } = useSWR(
|
||||
workspaceSlug ? ["projectAnalyticsCount", workspaceSlug] : null,
|
||||
workspaceSlug
|
||||
? () =>
|
||||
fetchProjectAnalyticsCount(workspaceSlug.toString(), {
|
||||
fields: "total_work_items,total_completed_work_items",
|
||||
})
|
||||
: null
|
||||
);
|
||||
return (
|
||||
<AnalyticsSectionWrapper
|
||||
title={`${t("workspace_analytics.active_projects")}`}
|
||||
subtitle={selectedDurationLabel}
|
||||
className="md:col-span-2"
|
||||
>
|
||||
<div className="flex flex-col gap-4 h-[350px] overflow-auto">
|
||||
{isProjectAnalyticsCountLoading &&
|
||||
Array.from({ length: 5 }).map((_, index) => <Loader.Item key={index} height="40px" width="100%" />)}
|
||||
{!isProjectAnalyticsCountLoading &&
|
||||
projectAnalyticsCount?.map((project) => <ActiveProjectItem key={project.id} project={project} />)}
|
||||
</div>
|
||||
</AnalyticsSectionWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default ActiveProjects;
|
||||
1
apps/web/core/components/analytics/overview/index.ts
Normal file
1
apps/web/core/components/analytics/overview/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
117
apps/web/core/components/analytics/overview/project-insights.tsx
Normal file
117
apps/web/core/components/analytics/overview/project-insights.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { observer } from "mobx-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane package imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { TChartData } from "@plane/types";
|
||||
// hooks
|
||||
import { useAnalytics } from "@/hooks/store/use-analytics";
|
||||
// services
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { AnalyticsService } from "@/services/analytics.service";
|
||||
// plane web components
|
||||
import AnalyticsSectionWrapper from "../analytics-section-wrapper";
|
||||
import AnalyticsEmptyState from "../empty-state";
|
||||
import { ProjectInsightsLoader } from "../loaders";
|
||||
|
||||
const RadarChart = dynamic(() =>
|
||||
import("@plane/propel/charts/radar-chart").then((mod) => ({
|
||||
default: mod.RadarChart,
|
||||
}))
|
||||
);
|
||||
|
||||
const analyticsService = new AnalyticsService();
|
||||
|
||||
const ProjectInsights = observer(() => {
|
||||
const params = useParams();
|
||||
const { t } = useTranslation();
|
||||
const workspaceSlug = params.workspaceSlug.toString();
|
||||
const { selectedDuration, selectedDurationLabel, selectedProjects, selectedCycle, selectedModule, isPeekView } =
|
||||
useAnalytics();
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/analytics/empty-chart-radar" });
|
||||
|
||||
const { data: projectInsightsData, isLoading: isLoadingProjectInsight } = useSWR(
|
||||
`radar-chart-project-insights-${workspaceSlug}-${selectedDuration}-${selectedProjects}-${selectedCycle}-${selectedModule}-${isPeekView}`,
|
||||
() =>
|
||||
analyticsService.getAdvanceAnalyticsCharts<TChartData<string, string>[]>(
|
||||
workspaceSlug,
|
||||
"projects",
|
||||
{
|
||||
// date_filter: selectedDuration,
|
||||
...(selectedProjects?.length > 0 && { project_ids: selectedProjects?.join(",") }),
|
||||
...(selectedCycle ? { cycle_id: selectedCycle } : {}),
|
||||
...(selectedModule ? { module_id: selectedModule } : {}),
|
||||
},
|
||||
isPeekView
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<AnalyticsSectionWrapper
|
||||
title={`${t("workspace_analytics.project_insights")}`}
|
||||
subtitle={selectedDurationLabel}
|
||||
className="md:col-span-3"
|
||||
>
|
||||
{isLoadingProjectInsight ? (
|
||||
<ProjectInsightsLoader />
|
||||
) : projectInsightsData && projectInsightsData?.length == 0 ? (
|
||||
<AnalyticsEmptyState
|
||||
title={t("workspace_analytics.empty_state.project_insights.title")}
|
||||
description={t("workspace_analytics.empty_state.project_insights.description")}
|
||||
className="h-[300px]"
|
||||
assetPath={resolvedPath}
|
||||
/>
|
||||
) : (
|
||||
<div className="gap-8 lg:flex">
|
||||
{projectInsightsData && (
|
||||
<RadarChart
|
||||
className="h-[350px] w-full lg:w-3/5"
|
||||
data={projectInsightsData}
|
||||
dataKey="key"
|
||||
radars={[
|
||||
{
|
||||
key: "count",
|
||||
name: "Count",
|
||||
fill: "rgba(var(--color-primary-300))",
|
||||
stroke: "rgba(var(--color-primary-300))",
|
||||
fillOpacity: 0.6,
|
||||
dot: {
|
||||
r: 4,
|
||||
fillOpacity: 1,
|
||||
},
|
||||
},
|
||||
]}
|
||||
margin={{ top: 0, right: 40, bottom: 10, left: 40 }}
|
||||
showTooltip
|
||||
angleAxis={{
|
||||
key: "name",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="w-full lg:w-2/5">
|
||||
<div className="text-sm text-custom-text-300">{t("workspace_analytics.summary_of_projects")}</div>
|
||||
<div className=" mb-3 border-b border-custom-border-100 py-2">{t("workspace_analytics.all_projects")}</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between text-sm text-custom-text-300">
|
||||
<div>{t("workspace_analytics.trend_on_charts")}</div>
|
||||
<div>{t("common.work_items")}</div>
|
||||
</div>
|
||||
{projectInsightsData?.map((item) => (
|
||||
<div key={item.key} className="flex items-center justify-between text-sm text-custom-text-100">
|
||||
<div>{item.name}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* <TrendPiece key={item.key} size='xs' /> */}
|
||||
<div className="text-custom-text-200">{item.count}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AnalyticsSectionWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
export default ProjectInsights;
|
||||
19
apps/web/core/components/analytics/overview/root.tsx
Normal file
19
apps/web/core/components/analytics/overview/root.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import AnalyticsWrapper from "../analytics-wrapper";
|
||||
import TotalInsights from "../total-insights";
|
||||
import ActiveProjects from "./active-projects";
|
||||
import ProjectInsights from "./project-insights";
|
||||
|
||||
const Overview: React.FC = () => (
|
||||
<AnalyticsWrapper i18nTitle="common.overview">
|
||||
<div className="flex flex-col gap-14">
|
||||
<TotalInsights analyticsType="overview" />
|
||||
<div className="grid grid-cols-1 gap-14 md:grid-cols-5 ">
|
||||
<ProjectInsights />
|
||||
<ActiveProjects />
|
||||
</div>
|
||||
</div>
|
||||
</AnalyticsWrapper>
|
||||
);
|
||||
|
||||
export { Overview };
|
||||
Reference in New Issue
Block a user