mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
1a534a3c19
* chore: global bar graph component * chore: global pie graph component * chore: global line graph component * chore: removed unnecessary file * chore: refactored global chart components to accept all props * chore: function to convert response to chart data * chore: global calendar graph component added * chore: global scatter plot graph component * feat: analytics boilerplate created * chore: null value for segment and project * chore: clean up file * chore: change project query param key * chore: export, refresh buttons, analytics table * fix: analytics fetch key error * chore: show only integer values in the y-axis * chore: custom x-axis tick values and bar colors * refactor: divide analytics page into granular components * chore: convert analytics page to modal, save analytics modal * fix: build error * fix: modal overflow issues, analytics loading screen * chore: custom tooltip, refactor: graphs folder structure * refactor: folder structure, chore: x-axis tick values for larger data * chore: code cleanup * chore: remove unnecessary files * fix: refresh analytics button on error * feat: scope and demand analytics * refactor: scope and demand and custom analytics folder structure * fix: dynamic import type * chore: minor updates * feat: project, cycle and module level analytics * style: project analytics modal * fix: merge conflicts
233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
import { useRouter } from "next/router";
|
|
|
|
import { mutate } from "swr";
|
|
|
|
// react-hook-form
|
|
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
|
// services
|
|
import analyticsService from "services/analytics.service";
|
|
// hooks
|
|
import useProjects from "hooks/use-projects";
|
|
import useToast from "hooks/use-toast";
|
|
// ui
|
|
import { CustomMenu, CustomSelect, PrimaryButton } from "components/ui";
|
|
// icons
|
|
import { ArrowPathIcon, ArrowUpTrayIcon } from "@heroicons/react/24/outline";
|
|
// types
|
|
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData } from "types";
|
|
// fetch-keys
|
|
import { ANALYTICS } from "constants/fetch-keys";
|
|
// constants
|
|
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
|
|
|
|
type Props = {
|
|
analytics: IAnalyticsResponse | undefined;
|
|
params: IAnalyticsParams;
|
|
control: Control<IAnalyticsParams, any>;
|
|
setValue: UseFormSetValue<IAnalyticsParams>;
|
|
setSaveAnalyticsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
fullScreen: boolean;
|
|
isProjectLevel?: boolean;
|
|
};
|
|
|
|
export const AnalyticsSidebar: React.FC<Props> = ({
|
|
analytics,
|
|
params,
|
|
control,
|
|
setValue,
|
|
setSaveAnalyticsModal,
|
|
fullScreen,
|
|
isProjectLevel = false,
|
|
}) => {
|
|
const router = useRouter();
|
|
const { workspaceSlug } = router.query;
|
|
|
|
const { projects } = useProjects();
|
|
|
|
const { setToastAlert } = useToast();
|
|
|
|
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) =>
|
|
setToastAlert({
|
|
type: "success",
|
|
title: "Success!",
|
|
message: res.message,
|
|
})
|
|
)
|
|
.catch(() =>
|
|
setToastAlert({
|
|
type: "error",
|
|
title: "Error!",
|
|
message: "There was some error in exporting the analytics. Please try again.",
|
|
})
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`gap-4 p-5 ${
|
|
fullScreen ? "border-l border-brand-base bg-brand-sidebar h-full" : ""
|
|
}`}
|
|
>
|
|
<div className={`sticky top-5 ${fullScreen ? "space-y-4" : "space-y-2"}`}>
|
|
<div className="flex items-center justify-between gap-2 flex-shrink-0">
|
|
<h5 className="text-lg font-medium">
|
|
{analytics?.total ?? 0}{" "}
|
|
<span className="text-xs font-normal text-brand-secondary">issues</span>
|
|
</h5>
|
|
<CustomMenu ellipsis>
|
|
<CustomMenu.MenuItem
|
|
onClick={() => {
|
|
if (!workspaceSlug) return;
|
|
|
|
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<ArrowPathIcon className="h-3 w-3" />
|
|
Refresh
|
|
</div>
|
|
</CustomMenu.MenuItem>
|
|
<CustomMenu.MenuItem onClick={exportAnalytics}>
|
|
<div className="flex items-center gap-2">
|
|
<ArrowUpTrayIcon className="h-3 w-3" />
|
|
Export analytics as CSV
|
|
</div>
|
|
</CustomMenu.MenuItem>
|
|
</CustomMenu>
|
|
</div>
|
|
<div className={`${fullScreen ? "space-y-4" : "grid items-center gap-4 grid-cols-3"}`}>
|
|
{isProjectLevel === false && (
|
|
<div>
|
|
<h6 className="text-xs text-brand-secondary">Project</h6>
|
|
<Controller
|
|
name="project"
|
|
control={control}
|
|
render={({ field: { value, onChange } }) => (
|
|
<CustomSelect
|
|
value={value}
|
|
label={projects.find((p) => p.id === value)?.name ?? "All projects"}
|
|
onChange={onChange}
|
|
width="w-full"
|
|
maxHeight="lg"
|
|
>
|
|
<CustomSelect.Option value={null}>All projects</CustomSelect.Option>
|
|
{projects.map((project) => (
|
|
<CustomSelect.Option key={project.id} value={project.id}>
|
|
{project.name}
|
|
</CustomSelect.Option>
|
|
))}
|
|
</CustomSelect>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<h6 className="text-xs text-brand-secondary">Measure (y-axis)</h6>
|
|
<Controller
|
|
name="y_axis"
|
|
control={control}
|
|
render={({ field: { value, onChange } }) => (
|
|
<CustomSelect
|
|
value={value}
|
|
label={
|
|
<span>
|
|
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}
|
|
</span>
|
|
}
|
|
onChange={onChange}
|
|
width="w-full"
|
|
>
|
|
{ANALYTICS_Y_AXIS_VALUES.map((item) => (
|
|
<CustomSelect.Option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</CustomSelect.Option>
|
|
))}
|
|
</CustomSelect>
|
|
)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h6 className="text-xs text-brand-secondary">Dimension (x-axis)</h6>
|
|
<Controller
|
|
name="x_axis"
|
|
control={control}
|
|
render={({ field: { value, onChange } }) => (
|
|
<CustomSelect
|
|
value={value}
|
|
label={
|
|
<span>{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label}</span>
|
|
}
|
|
onChange={(val: string) => {
|
|
if (params.segment === val) setValue("segment", null);
|
|
|
|
onChange(val);
|
|
}}
|
|
width="w-full"
|
|
maxHeight="lg"
|
|
>
|
|
{ANALYTICS_X_AXIS_VALUES.map((item) => (
|
|
<CustomSelect.Option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</CustomSelect.Option>
|
|
))}
|
|
</CustomSelect>
|
|
)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<h6 className="text-xs text-brand-secondary">Segment</h6>
|
|
<Controller
|
|
name="segment"
|
|
control={control}
|
|
render={({ field: { value, onChange } }) => (
|
|
<CustomSelect
|
|
value={value}
|
|
label={
|
|
<span>
|
|
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? (
|
|
<span className="text-brand-secondary">No value</span>
|
|
)}
|
|
</span>
|
|
}
|
|
onChange={onChange}
|
|
width="w-full"
|
|
maxHeight="lg"
|
|
>
|
|
<CustomSelect.Option value={null}>No value</CustomSelect.Option>
|
|
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
|
if (params.x_axis === item.value) return null;
|
|
|
|
return (
|
|
<CustomSelect.Option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</CustomSelect.Option>
|
|
);
|
|
})}
|
|
</CustomSelect>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* <div className="flex items-center justify-end gap-2">
|
|
<PrimaryButton className="py-1" onClick={() => setSaveAnalyticsModal(true)}>
|
|
Save analytics
|
|
</PrimaryButton>
|
|
</div> */}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|