forked from github/plane
style: new custom analytics ui (#1055)
This commit is contained in:
parent
8c707cc544
commit
c6d78b5e6a
@ -62,7 +62,7 @@ export const CreateUpdateAnalyticsModal: React.FC<Props> = ({ isOpen, handleClos
|
||||
x_axis: "priority",
|
||||
y_axis: "issue_count",
|
||||
...params,
|
||||
project: params?.project ? [params.project] : [],
|
||||
project: params?.project ?? [],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,144 +1,130 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
import { Control, UseFormSetValue } from "react-hook-form";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
// components
|
||||
import {
|
||||
AnalyticsGraph,
|
||||
AnalyticsSelectBar,
|
||||
AnalyticsSidebar,
|
||||
AnalyticsTable,
|
||||
CreateUpdateAnalyticsModal,
|
||||
} from "components/analytics";
|
||||
// ui
|
||||
import { Loader, PrimaryButton } from "components/ui";
|
||||
// helpers
|
||||
import { convertResponseToBarGraphData } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams } from "types";
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
const defaultValues: IAnalyticsParams = {
|
||||
x_axis: "priority",
|
||||
y_axis: "issue_count",
|
||||
segment: null,
|
||||
project: null,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isProjectLevel?: boolean;
|
||||
fullScreen?: boolean;
|
||||
analytics: IAnalyticsResponse | undefined;
|
||||
analyticsError: any;
|
||||
params: IAnalyticsParams;
|
||||
control: Control<IAnalyticsParams, any>;
|
||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
export const CustomAnalytics: React.FC<Props> = ({ isProjectLevel = false, fullScreen = true }) => {
|
||||
const [saveAnalyticsModal, setSaveAnalyticsModal] = useState(false);
|
||||
|
||||
export const CustomAnalytics: React.FC<Props> = ({
|
||||
analytics,
|
||||
analyticsError,
|
||||
params,
|
||||
control,
|
||||
setValue,
|
||||
fullScreen,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
|
||||
const isProjectLevel = projectId ? true : false;
|
||||
|
||||
const params: IAnalyticsParams = {
|
||||
x_axis: watch("x_axis"),
|
||||
y_axis: watch("y_axis"),
|
||||
segment: watch("segment"),
|
||||
project: isProjectLevel ? projectId?.toString() : watch("project"),
|
||||
cycle: isProjectLevel && cycleId ? cycleId.toString() : null,
|
||||
module: isProjectLevel && moduleId ? moduleId.toString() : null,
|
||||
};
|
||||
|
||||
const {
|
||||
data: analytics,
|
||||
error: analyticsError,
|
||||
mutate: mutateAnalytics,
|
||||
} = useSWR(
|
||||
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
|
||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
||||
);
|
||||
|
||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "effort";
|
||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate";
|
||||
const barGraphData = convertResponseToBarGraphData(analytics?.distribution, params);
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateAnalyticsModal
|
||||
isOpen={saveAnalyticsModal}
|
||||
handleClose={() => setSaveAnalyticsModal(false)}
|
||||
params={params}
|
||||
/>
|
||||
<div
|
||||
className={`overflow-y-auto ${
|
||||
fullScreen ? "grid grid-cols-4 h-full" : "flex flex-col-reverse"
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-3">
|
||||
{!analyticsError ? (
|
||||
analytics ? (
|
||||
analytics.total > 0 ? (
|
||||
<>
|
||||
<AnalyticsGraph
|
||||
analytics={analytics}
|
||||
barGraphData={barGraphData}
|
||||
params={params}
|
||||
yAxisKey={yAxisKey}
|
||||
fullScreen={fullScreen}
|
||||
/>
|
||||
<AnalyticsTable
|
||||
analytics={analytics}
|
||||
barGraphData={barGraphData}
|
||||
params={params}
|
||||
yAxisKey={yAxisKey}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-brand-secondary">
|
||||
<p className="text-sm">
|
||||
No matching issues found. Try changing the parameters.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<div
|
||||
className={`overflow-hidden flex flex-col-reverse ${
|
||||
fullScreen ? "md:grid md:grid-cols-4 md:h-full" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="col-span-3 flex flex-col h-full overflow-hidden">
|
||||
<AnalyticsSelectBar
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
projects={projects}
|
||||
params={params}
|
||||
fullScreen={fullScreen}
|
||||
isProjectLevel={isProjectLevel}
|
||||
/>
|
||||
{!analyticsError ? (
|
||||
analytics ? (
|
||||
analytics.total > 0 ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<AnalyticsGraph
|
||||
analytics={analytics}
|
||||
barGraphData={barGraphData}
|
||||
params={params}
|
||||
yAxisKey={yAxisKey}
|
||||
fullScreen={fullScreen}
|
||||
/>
|
||||
<AnalyticsTable
|
||||
analytics={analytics}
|
||||
barGraphData={barGraphData}
|
||||
params={params}
|
||||
yAxisKey={yAxisKey}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="space-y-6 p-5">
|
||||
<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-brand-secondary">
|
||||
<p className="text-sm">There was some error in fetching the data.</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<PrimaryButton onClick={() => mutateAnalytics()}>Refresh</PrimaryButton>
|
||||
<div className="grid h-full place-items-center p-5">
|
||||
<div className="space-y-4 text-brand-secondary">
|
||||
<p className="text-sm">No matching issues found. Try changing the parameters.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<Loader className="space-y-6 p-5">
|
||||
<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-brand-secondary">
|
||||
<p className="text-sm">There was some error in fetching the data.</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={fullScreen ? "h-full" : ""}>
|
||||
<AnalyticsSidebar
|
||||
analytics={analytics}
|
||||
params={params}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
setSaveAnalyticsModal={setSaveAnalyticsModal}
|
||||
fullScreen={fullScreen}
|
||||
isProjectLevel={isProjectLevel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<AnalyticsSidebar
|
||||
analytics={analytics}
|
||||
params={params}
|
||||
fullScreen={fullScreen}
|
||||
isProjectLevel={isProjectLevel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -18,11 +18,11 @@ export const CustomTooltip: React.FC<Props> = ({ datum, params }) => {
|
||||
else tooltipValue = datum.id;
|
||||
} else {
|
||||
if (DATE_KEYS.includes(params.x_axis)) tooltipValue = datum.indexValue;
|
||||
else tooltipValue = datum.id === "count" ? "Issue count" : "Effort";
|
||||
else tooltipValue = datum.id === "count" ? "Issue count" : "Estimate";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-base p-2 text-xs">
|
||||
<div className="flex items-center gap-2 rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
||||
<span
|
||||
className="h-3 w-3 rounded"
|
||||
style={{
|
||||
|
@ -18,7 +18,7 @@ type Props = {
|
||||
xAxisKeys: string[];
|
||||
};
|
||||
params: IAnalyticsParams;
|
||||
yAxisKey: "effort" | "count";
|
||||
yAxisKey: "count" | "estimate";
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
@ -70,6 +70,7 @@ export const AnalyticsGraph: React.FC<Props> = ({
|
||||
height={fullScreen ? "400px" : "300px"}
|
||||
margin={{ right: 20, bottom: longestXAxisLabel.length * 5 + 20 }}
|
||||
theme={{
|
||||
background: "rgb(var(--color-bg-base))",
|
||||
axis: {},
|
||||
}}
|
||||
/>
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from "./graph";
|
||||
export * from "./create-update-analytics-modal";
|
||||
export * from "./custom-analytics";
|
||||
export * from "./select-bar";
|
||||
export * from "./sidebar";
|
||||
export * from "./table";
|
||||
|
@ -0,0 +1,80 @@
|
||||
// react-hook-form
|
||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
||||
// components
|
||||
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
|
||||
// types
|
||||
import { IAnalyticsParams, IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
control: Control<IAnalyticsParams, any>;
|
||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||
projects: IProject[];
|
||||
params: IAnalyticsParams;
|
||||
fullScreen: boolean;
|
||||
isProjectLevel: boolean;
|
||||
};
|
||||
|
||||
export const AnalyticsSelectBar: React.FC<Props> = ({
|
||||
control,
|
||||
setValue,
|
||||
projects,
|
||||
params,
|
||||
fullScreen,
|
||||
isProjectLevel,
|
||||
}) => (
|
||||
<div
|
||||
className={`grid items-center gap-4 p-5 pb-0.5 ${
|
||||
isProjectLevel ? "grid-cols-3" : "grid-cols-2"
|
||||
} ${fullScreen ? "lg:grid-cols-4" : ""}`}
|
||||
>
|
||||
{!isProjectLevel && (
|
||||
<div>
|
||||
<h6 className="text-xs text-brand-secondary">Project</h6>
|
||||
<Controller
|
||||
name="project"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectProject value={value} onChange={onChange} projects={projects} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h6 className="text-xs text-brand-secondary">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-brand-secondary">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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="text-xs text-brand-secondary">Group</h6>
|
||||
<Controller
|
||||
name="segment"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SelectSegment value={value} onChange={onChange} params={params} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
@ -1,51 +1,82 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
import projectService from "services/project.service";
|
||||
import cyclesService from "services/cycles.service";
|
||||
import modulesService from "services/modules.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { CustomMenu, CustomSelect, PrimaryButton } from "components/ui";
|
||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// icons
|
||||
import { ArrowPathIcon, ArrowUpTrayIcon } from "@heroicons/react/24/outline";
|
||||
import { ArrowDownTrayIcon, ArrowPathIcon, UserGroupIcon } from "@heroicons/react/24/outline";
|
||||
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
|
||||
// helpers
|
||||
import { renderShortDate } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData } from "types";
|
||||
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IProject } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
import { ANALYTICS, CYCLE_DETAILS, MODULE_DETAILS, PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
|
||||
import { NETWORK_CHOICES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse | undefined;
|
||||
params: IAnalyticsParams;
|
||||
control: Control<IAnalyticsParams, any>;
|
||||
setValue: UseFormSetValue<IAnalyticsParams>;
|
||||
setSaveAnalyticsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
fullScreen: boolean;
|
||||
isProjectLevel?: 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 { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { projects } = useProjects();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? PROJECT_DETAILS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && !(cycleId || moduleId)
|
||||
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleDetails } = useSWR(
|
||||
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
|
||||
workspaceSlug && projectId && cycleId
|
||||
? () =>
|
||||
cyclesService.getCycleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
cycleId.toString()
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: moduleDetails } = useSWR(
|
||||
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
|
||||
workspaceSlug && projectId && moduleId
|
||||
? () =>
|
||||
modulesService.getModuleDetails(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
moduleId.toString()
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const exportAnalytics = () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
@ -55,7 +86,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
if (params.segment) data.segment = params.segment;
|
||||
if (params.project) data.project = [params.project];
|
||||
if (params.project) data.project = params.project;
|
||||
|
||||
analyticsService
|
||||
.exportAnalytics(workspaceSlug.toString(), data)
|
||||
@ -77,155 +108,180 @@ export const AnalyticsSidebar: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`gap-4 p-5 ${
|
||||
fullScreen ? "border-l border-brand-base bg-brand-sidebar h-full" : ""
|
||||
className={`p-5 pb-0 flex flex-col space-y-2 md:space-y-4 overflow-hidden ${
|
||||
fullScreen
|
||||
? "pb-5 border-l border-brand-base md:h-full md:pb-5 md:border-l md:border-brand-base"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<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 className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-1 bg-brand-surface-2 rounded-md px-3 py-1 text-brand-secondary text-xs">
|
||||
<LayerDiagonalIcon height={14} width={14} />
|
||||
{analytics ? analytics.total : "..."} Issues
|
||||
</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;
|
||||
</div>
|
||||
<div className="h-full overflow-hidden">
|
||||
{fullScreen ? (
|
||||
<>
|
||||
{!isProjectLevel && params.project && params.project.length > 0 && (
|
||||
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
|
||||
<h4 className="font-medium">Selected Projects</h4>
|
||||
<div className="space-y-6 mt-4 h-full overflow-y-auto">
|
||||
{params.project.map((projectId) => {
|
||||
const project: IProject = projects.find((p) => p.id === projectId);
|
||||
|
||||
return (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
<div key={project.id}>
|
||||
<h5 className="text-sm flex items-center gap-1">
|
||||
{project.icon ? (
|
||||
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
||||
{String.fromCodePoint(parseInt(project.icon))}
|
||||
</span>
|
||||
) : (
|
||||
<span className="grid h-8 w-8 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
{project?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<span className="break-all">{project.name}</span>
|
||||
</h5>
|
||||
<div className="mt-4 space-y-3 pl-2">
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserGroupIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<h6>Total members</h6>
|
||||
</div>
|
||||
<span className="text-brand-secondary">{project.total_members}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<ContrastIcon height={16} width={16} />
|
||||
<h6>Total cycles</h6>
|
||||
</div>
|
||||
<span className="text-brand-secondary">{project.total_cycles}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserGroupIcon className="h-4 w-4 text-brand-secondary" />
|
||||
<h6>Total modules</h6>
|
||||
</div>
|
||||
<span className="text-brand-secondary">{project.total_modules}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{projectId ? (
|
||||
cycleId && cycleDetails ? (
|
||||
<div className="hidden md:block h-full overflow-y-auto">
|
||||
<h4 className="font-medium break-all">{cycleDetails.name}</h4>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-brand-secondary">Lead</h6>
|
||||
<span>
|
||||
{cycleDetails.owned_by?.first_name} {cycleDetails.owned_by?.last_name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-brand-secondary">Start Date</h6>
|
||||
<span>
|
||||
{cycleDetails.start_date && cycleDetails.start_date !== ""
|
||||
? renderShortDate(cycleDetails.start_date)
|
||||
: "No start date"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-brand-secondary">Target Date</h6>
|
||||
<span>
|
||||
{cycleDetails.end_date && cycleDetails.end_date !== ""
|
||||
? renderShortDate(cycleDetails.end_date)
|
||||
: "No end date"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : moduleId && moduleDetails ? (
|
||||
<div className="hidden md:block h-full overflow-y-auto">
|
||||
<h4 className="font-medium break-all">{moduleDetails.name}</h4>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-brand-secondary">Lead</h6>
|
||||
<span>
|
||||
{moduleDetails.lead_detail?.first_name}{" "}
|
||||
{moduleDetails.lead_detail?.last_name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-brand-secondary">Start Date</h6>
|
||||
<span>
|
||||
{moduleDetails.start_date && moduleDetails.start_date !== ""
|
||||
? renderShortDate(moduleDetails.start_date)
|
||||
: "No start date"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-brand-secondary">Target Date</h6>
|
||||
<span>
|
||||
{moduleDetails.target_date && moduleDetails.target_date !== ""
|
||||
? renderShortDate(moduleDetails.target_date)
|
||||
: "No end date"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:flex md:flex-col h-full overflow-y-auto">
|
||||
<div className="flex items-center gap-1">
|
||||
{projectDetails?.icon ? (
|
||||
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">
|
||||
{String.fromCodePoint(parseInt(projectDetails.icon))}
|
||||
</span>
|
||||
) : (
|
||||
<span className="grid h-8 w-8 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
|
||||
{projectDetails?.name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
<h4 className="font-medium break-all">{projectDetails?.name}</h4>
|
||||
</div>
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<h6 className="text-brand-secondary">Network</h6>
|
||||
<span>
|
||||
{
|
||||
NETWORK_CHOICES[
|
||||
`${projectDetails?.network}` as keyof typeof NETWORK_CHOICES
|
||||
]
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap justify-self-end">
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 -my-1">
|
||||
<ArrowPathIcon className="h-3.5 w-3.5" />
|
||||
Refresh
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-end gap-2">
|
||||
<PrimaryButton className="py-1" onClick={() => setSaveAnalyticsModal(true)}>
|
||||
Save analytics
|
||||
</PrimaryButton>
|
||||
</div> */}
|
||||
</SecondaryButton>
|
||||
<PrimaryButton onClick={exportAnalytics}>
|
||||
<div className="flex items-center gap-2 -my-1">
|
||||
<ArrowDownTrayIcon className="h-3.5 w-3.5" />
|
||||
Export as CSV
|
||||
</div>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,7 +10,6 @@ import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
|
||||
import { MONTHS_LIST } from "constants/calendar";
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse;
|
||||
@ -19,7 +18,7 @@ type Props = {
|
||||
xAxisKeys: string[];
|
||||
};
|
||||
params: IAnalyticsParams;
|
||||
yAxisKey: "effort" | "count";
|
||||
yAxisKey: "count" | "estimate";
|
||||
};
|
||||
|
||||
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => (
|
||||
@ -27,7 +26,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<table className="min-w-full divide-y divide-brand-base whitespace-nowrap border-y border-brand-base">
|
||||
<thead className="bg-brand-base">
|
||||
<thead className="bg-brand-surface-2">
|
||||
<tr className="divide-x divide-brand-base text-sm text-brand-base">
|
||||
<th scope="col" className="py-3 px-2.5 text-left font-medium">
|
||||
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export * from "./custom-analytics";
|
||||
export * from "./scope-and-demand";
|
||||
export * from "./select";
|
||||
export * from "./project-modal";
|
||||
export * from "./workspace-modal";
|
||||
|
@ -1,7 +1,15 @@
|
||||
import React, { Fragment, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
// icons
|
||||
@ -10,17 +18,47 @@ import {
|
||||
ArrowsPointingOutIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IAnalyticsParams } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: IAnalyticsParams = {
|
||||
x_axis: "priority",
|
||||
y_axis: "issue_count",
|
||||
segment: null,
|
||||
project: null,
|
||||
};
|
||||
|
||||
const tabsList = ["Scope and Demand", "Custom Analytics"];
|
||||
|
||||
export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
|
||||
|
||||
const params: IAnalyticsParams = {
|
||||
x_axis: watch("x_axis"),
|
||||
y_axis: watch("y_axis"),
|
||||
segment: watch("segment"),
|
||||
project: projectId ? [projectId.toString()] : watch("project"),
|
||||
cycle: cycleId ? cycleId.toString() : null,
|
||||
module: moduleId ? moduleId.toString() : null,
|
||||
};
|
||||
|
||||
const { data: analytics, error: analyticsError } = useSWR(
|
||||
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
|
||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
@ -32,12 +70,12 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-full flex-col overflow-hidden border-brand-base bg-brand-surface-1 text-left ${
|
||||
className={`flex h-full flex-col overflow-hidden border-brand-base bg-brand-base text-left ${
|
||||
fullScreen ? "rounded-lg border" : "border-l"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-between gap-2 border-b border-b-brand-base bg-brand-sidebar p-3 text-sm ${
|
||||
className={`flex items-center justify-between gap-2 border-b border-b-brand-base bg-brand-base p-3 text-sm ${
|
||||
fullScreen ? "" : "py-[1.275rem]"
|
||||
}`}
|
||||
>
|
||||
@ -64,13 +102,13 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
</div>
|
||||
</div>
|
||||
<Tab.Group as={Fragment}>
|
||||
<Tab.List className="space-x-2 border-b border-brand-base px-5 py-3">
|
||||
<Tab.List as="div" className="space-x-2 border-b border-brand-base px-5 py-3">
|
||||
{tabsList.map((tab) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-base ${
|
||||
selected ? "bg-brand-base" : ""
|
||||
`rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-surface-2 ${
|
||||
selected ? "bg-brand-surface-2" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
@ -80,10 +118,17 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
</Tab.List>
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<ScopeAndDemand fullScreen={fullScreen} isProjectLevel />
|
||||
<ScopeAndDemand fullScreen={fullScreen} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<CustomAnalytics fullScreen={fullScreen} isProjectLevel />
|
||||
<CustomAnalytics
|
||||
analytics={analytics}
|
||||
analyticsError={analyticsError}
|
||||
params={params}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
fullScreen={fullScreen}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
@ -50,7 +50,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-brand-base bg-brand-base p-2 text-xs">
|
||||
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
||||
<p className="flex items-center gap-1 text-brand-secondary">
|
||||
<PlayIcon className="h-4 w-4 -rotate-90" aria-hidden="true" />
|
||||
<span>Estimate Demand:</span>
|
||||
|
@ -3,7 +3,8 @@ import Image from "next/image";
|
||||
type Props = {
|
||||
users: {
|
||||
avatar: string | null;
|
||||
email: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
count: number;
|
||||
}[];
|
||||
title: string;
|
||||
@ -13,8 +14,8 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||
<div className="p-3 border border-brand-base rounded-[10px]">
|
||||
<h6 className="text-base font-medium">{title}</h6>
|
||||
<div className="mt-3 space-y-3">
|
||||
{users.map((user) => (
|
||||
<div key={user.email} className="flex items-start justify-between gap-4 text-xs">
|
||||
{users.map((user, index) => (
|
||||
<div key={`user-${index}`} className="flex items-start justify-between gap-4 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
{user && user.avatar && user.avatar !== "" ? (
|
||||
<div className="rounded-full h-4 w-4 flex-shrink-0">
|
||||
@ -23,15 +24,17 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={user.email ?? "No assignee"}
|
||||
alt={user.firstName + " " + user.lastName}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid place-items-center flex-shrink-0 rounded-full bg-gray-700 text-[11px] capitalize text-white h-4 w-4">
|
||||
{(user.email ?? "No assignee").charAt(0)}
|
||||
{user.firstName !== "" ? user.firstName[0] : "?"}
|
||||
</div>
|
||||
)}
|
||||
<span className="break-all text-brand-secondary">{user.email ?? "No assignee"}</span>
|
||||
<span className="break-all text-brand-secondary">
|
||||
{user.firstName !== "" ? `${user.firstName} ${user.lastName}` : "No assignee"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-shrink-0">{user.count}</span>
|
||||
</div>
|
||||
|
@ -18,16 +18,17 @@ import { DEFAULT_ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
fullScreen?: boolean;
|
||||
isProjectLevel?: boolean;
|
||||
};
|
||||
|
||||
export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true, isProjectLevel = true }) => {
|
||||
export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const isProjectLevel = projectId ? true : false;
|
||||
|
||||
const params = isProjectLevel
|
||||
? {
|
||||
project: projectId ? projectId.toString() : null,
|
||||
project: projectId ? [projectId.toString()] : null,
|
||||
cycle: cycleId ? cycleId.toString() : null,
|
||||
module: moduleId ? moduleId.toString() : null,
|
||||
}
|
||||
@ -55,7 +56,8 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true, isProjectLe
|
||||
<AnalyticsLeaderboard
|
||||
users={defaultAnalytics.most_issue_created_user.map((user) => ({
|
||||
avatar: user.created_by__avatar,
|
||||
email: user.created_by__email,
|
||||
firstName: user.created_by__first_name,
|
||||
lastName: user.created_by__last_name,
|
||||
count: user.count,
|
||||
}))}
|
||||
title="Most issues created"
|
||||
@ -63,7 +65,8 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true, isProjectLe
|
||||
<AnalyticsLeaderboard
|
||||
users={defaultAnalytics.most_issue_closed_user.map((user) => ({
|
||||
avatar: user.assignees__avatar,
|
||||
email: user.assignees__email,
|
||||
firstName: user.assignees__first_name,
|
||||
lastName: user.assignees__last_name,
|
||||
count: user.count,
|
||||
}))}
|
||||
title="Most issues closed"
|
||||
|
@ -21,7 +21,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
colors={() => `#f97316`}
|
||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
|
||||
tooltip={(datum) => (
|
||||
<div className="rounded-md border border-brand-base bg-brand-base p-2 text-xs">
|
||||
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
|
||||
<span className="font-medium text-brand-secondary">
|
||||
Issue count- {datum.indexValue ?? "No assignee"}:{" "}
|
||||
</span>
|
||||
@ -51,13 +51,17 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<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}` ?? "No assignee").toUpperCase().charAt(0)}
|
||||
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
},
|
||||
}}
|
||||
margin={{ top: 20 }}
|
||||
theme={{
|
||||
background: "rgb(var(--color-bg-base))",
|
||||
axis: {},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,6 +40,9 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
|
||||
colors={(datum) => datum.color}
|
||||
curve="monotoneX"
|
||||
margin={{ top: 20 }}
|
||||
theme={{
|
||||
background: "rgb(var(--color-bg-base))",
|
||||
}}
|
||||
enableArea
|
||||
/>
|
||||
</div>
|
||||
|
4
apps/app/components/analytics/select/index.ts
Normal file
4
apps/app/components/analytics/select/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./project";
|
||||
export * from "./segment";
|
||||
export * from "./x-axis";
|
||||
export * from "./y-axis";
|
38
apps/app/components/analytics/select/project.tsx
Normal file
38
apps/app/components/analytics/select/project.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
// ui
|
||||
import { CustomSearchSelect } from "components/ui";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
value: string[] | null | undefined;
|
||||
onChange: (val: string[] | null) => void;
|
||||
projects: IProject[];
|
||||
};
|
||||
|
||||
export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) => {
|
||||
const options = projects?.map((project) => ({
|
||||
value: project.id,
|
||||
query: project.name + project.identifier,
|
||||
content: <>{project.name}</>,
|
||||
}));
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
value={value ?? []}
|
||||
onChange={(val: string[]) => onChange(val)}
|
||||
options={options}
|
||||
label={
|
||||
value && value.length > 0
|
||||
? projects
|
||||
.filter((p) => value.includes(p.id))
|
||||
.map((p) => p.identifier)
|
||||
.join(", ")
|
||||
: "All projects"
|
||||
}
|
||||
optionsClassName="min-w-full"
|
||||
position="right"
|
||||
noChevron
|
||||
multiple
|
||||
/>
|
||||
);
|
||||
};
|
39
apps/app/components/analytics/select/segment.tsx
Normal file
39
apps/app/components/analytics/select/segment.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// types
|
||||
import { IAnalyticsParams, TXAxisValues } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";
|
||||
|
||||
type Props = {
|
||||
value: TXAxisValues | null | undefined;
|
||||
onChange: () => void;
|
||||
params: IAnalyticsParams;
|
||||
};
|
||||
|
||||
export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => (
|
||||
<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>
|
||||
);
|
27
apps/app/components/analytics/select/x-axis.tsx
Normal file
27
apps/app/components/analytics/select/x-axis.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// types
|
||||
import { IAnalyticsParams, TXAxisValues, TYAxisValues } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
|
||||
|
||||
type Props = {
|
||||
value: TXAxisValues;
|
||||
onChange: (val: string) => void;
|
||||
};
|
||||
|
||||
export const SelectXAxis: React.FC<Props> = ({ value, onChange }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={<span>{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label}</span>}
|
||||
onChange={onChange}
|
||||
width="w-full"
|
||||
maxHeight="lg"
|
||||
>
|
||||
{ANALYTICS_X_AXIS_VALUES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
26
apps/app/components/analytics/select/y-axis.tsx
Normal file
26
apps/app/components/analytics/select/y-axis.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
// ui
|
||||
import { CustomSelect } from "components/ui";
|
||||
// types
|
||||
import { IAnalyticsParams, TYAxisValues } from "types";
|
||||
// constants
|
||||
import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";
|
||||
|
||||
type Props = {
|
||||
value: TYAxisValues;
|
||||
onChange: () => void;
|
||||
};
|
||||
|
||||
export const SelectYAxis: React.FC<Props> = ({ 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>
|
||||
);
|
@ -1,70 +0,0 @@
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const tabsList = ["Scope and Demand", "Custom Analytics"];
|
||||
|
||||
export const AnalyticsWorkspaceModal: React.FC<Props> = ({ isOpen, onClose }) => {
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`absolute z-40 h-full w-full bg-brand-surface-1 p-2 ${
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full flex-col overflow-hidden rounded-lg border border-brand-base bg-brand-surface-1 text-left">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-b-brand-base bg-brand-sidebar p-3 text-sm">
|
||||
<h3>Workspace Analytics</h3>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center p-1 text-brand-secondary hover:text-brand-base"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Tab.Group as={Fragment}>
|
||||
<Tab.List className="space-x-2 border-b border-brand-base px-5 py-3">
|
||||
{tabsList.map((tab) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-base ${
|
||||
selected ? "bg-brand-base" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<ScopeAndDemand isProjectLevel={false} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<CustomAnalytics />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,6 +1,8 @@
|
||||
export const getPriorityIcon = (priority: string | null, className?: string) => {
|
||||
if (!className || className === "") className = "text-xs flex items-center";
|
||||
|
||||
priority = priority?.toLowerCase() ?? null;
|
||||
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return <span className={`material-symbols-rounded ${className}`}>error</span>;
|
||||
|
@ -6,18 +6,10 @@ import Link from "next/link";
|
||||
// hooks
|
||||
import useTheme from "hooks/use-theme";
|
||||
// icons
|
||||
import { GridViewIcon, AssignmentClipboardIcon, TickMarkIcon, SettingIcon } from "components/icons";
|
||||
import { ChartBarIcon } from "@heroicons/react/24/outline";
|
||||
import { GridViewIcon, AssignmentClipboardIcon, TickMarkIcon, SettingIcon } from "components/icons";
|
||||
|
||||
type Props = {
|
||||
isAnalyticsModalOpen: boolean;
|
||||
setAnalyticsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const WorkspaceSidebarMenu: React.FC<Props> = ({
|
||||
isAnalyticsModalOpen,
|
||||
setAnalyticsModal,
|
||||
}) => {
|
||||
export const WorkspaceSidebarMenu = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
@ -33,8 +25,7 @@ export const WorkspaceSidebarMenu: React.FC<Props> = ({
|
||||
{
|
||||
icon: ChartBarIcon,
|
||||
name: "Analytics",
|
||||
highlight: isAnalyticsModalOpen,
|
||||
onClick: () => setAnalyticsModal((prevData) => !prevData),
|
||||
href: `/${workspaceSlug}/analytics`,
|
||||
},
|
||||
{
|
||||
icon: AssignmentClipboardIcon,
|
||||
@ -55,59 +46,33 @@ export const WorkspaceSidebarMenu: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2 px-3 py-1">
|
||||
{workspaceLinks(workspaceSlug as string).map((link, index) => {
|
||||
if (link.href)
|
||||
return (
|
||||
<Link key={index} href={link.href}>
|
||||
<a
|
||||
className={`${
|
||||
(
|
||||
link.name === "Settings"
|
||||
? router.asPath.includes(link.href)
|
||||
: router.asPath === link.href
|
||||
)
|
||||
? "bg-brand-surface-2 text-brand-base"
|
||||
: "text-brand-secondary hover:bg-brand-surface-2 focus:bg-brand-surface-2"
|
||||
} group flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium outline-none ${
|
||||
sidebarCollapse ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
||||
<link.icon
|
||||
className="text-brand-secondary"
|
||||
aria-hidden="true"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
</span>
|
||||
{!sidebarCollapse && link.name}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`group flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium text-brand-secondary outline-none hover:bg-brand-surface-2 ${
|
||||
sidebarCollapse ? "justify-center" : ""
|
||||
} ${link.highlight ? "bg-brand-surface-2 text-brand-base" : ""}`}
|
||||
onClick={() => {
|
||||
if (link.onClick) link.onClick();
|
||||
}}
|
||||
>
|
||||
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
||||
<link.icon
|
||||
className="text-brand-secondary"
|
||||
aria-hidden="true"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
</span>
|
||||
{!sidebarCollapse && link.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{workspaceLinks(workspaceSlug as string).map((link, index) => (
|
||||
<Link key={index} href={link.href}>
|
||||
<a
|
||||
className={`${
|
||||
(
|
||||
link.name === "Settings"
|
||||
? router.asPath.includes(link.href)
|
||||
: router.asPath === link.href
|
||||
)
|
||||
? "bg-brand-surface-2 text-brand-base"
|
||||
: "text-brand-secondary hover:bg-brand-surface-2 focus:bg-brand-surface-2"
|
||||
} group flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium outline-none ${
|
||||
sidebarCollapse ? "justify-center" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
|
||||
<link.icon
|
||||
className="text-brand-secondary"
|
||||
aria-hidden="true"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
</span>
|
||||
{!sidebarCollapse && link.name}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -58,8 +58,8 @@ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] =
|
||||
label: "Issue Count",
|
||||
},
|
||||
{
|
||||
value: "effort",
|
||||
label: "Effort",
|
||||
value: "estimate",
|
||||
label: "Estimate",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -169,10 +169,10 @@ export const ESTIMATE_DETAILS = (estimateId: string) =>
|
||||
|
||||
// analytics
|
||||
export const ANALYTICS = (workspaceSlug: string, params: IAnalyticsParams) =>
|
||||
`ANALYTICS${workspaceSlug.toUpperCase()}_${params.x_axis}_${params.y_axis}_${params.segment}_${
|
||||
params.project
|
||||
}`;
|
||||
`ANALYTICS${workspaceSlug.toUpperCase()}_${params.x_axis}_${params.y_axis}_${
|
||||
params.segment
|
||||
}_${params.project?.toString()}`;
|
||||
export const DEFAULT_ANALYTICS = (workspaceSlug: string, params?: Partial<IAnalyticsParams>) =>
|
||||
`DEFAULT_ANALYTICS_${workspaceSlug.toUpperCase()}_${params?.project}_${params?.cycle}_${
|
||||
params?.module
|
||||
}`;
|
||||
`DEFAULT_ANALYTICS_${workspaceSlug.toUpperCase()}_${params?.project?.toString()}_${
|
||||
params?.cycle
|
||||
}_${params?.module}`;
|
||||
|
@ -1,5 +1,7 @@
|
||||
// nivo
|
||||
import { BarDatum } from "@nivo/bar";
|
||||
// helpers
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
// constants
|
||||
@ -17,7 +19,7 @@ export const convertResponseToBarGraphData = (
|
||||
const data: BarDatum[] = [];
|
||||
|
||||
let xAxisKeys: string[] = [];
|
||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "effort";
|
||||
const yAxisKey = params.y_axis === "issue_count" ? "count" : "estimate";
|
||||
|
||||
Object.keys(response).forEach((key) => {
|
||||
const segments: { [key: string]: number } = {};
|
||||
@ -31,7 +33,11 @@ export const convertResponseToBarGraphData = (
|
||||
});
|
||||
|
||||
data.push({
|
||||
name: DATE_KEYS.includes(params.x_axis) ? renderMonthAndYear(key) : key,
|
||||
name: DATE_KEYS.includes(params.x_axis)
|
||||
? renderMonthAndYear(key)
|
||||
: params.x_axis === "priority" || params.x_axis === "state__group"
|
||||
? capitalizeFirstLetter(key)
|
||||
: key,
|
||||
...segments,
|
||||
});
|
||||
} else {
|
||||
@ -42,6 +48,8 @@ export const convertResponseToBarGraphData = (
|
||||
data.push({
|
||||
name: DATE_KEYS.includes(params.x_axis)
|
||||
? renderMonthAndYear(item.dimension)
|
||||
: params.x_axis === "priority" || params.x_axis === "state__group"
|
||||
? capitalizeFirstLetter(item.dimension ?? "None")
|
||||
: item.dimension ?? "None",
|
||||
[yAxisKey]: item[yAxisKey] ?? 0,
|
||||
});
|
||||
@ -64,17 +72,17 @@ export const generateBarColor = (
|
||||
if (params[type] === "state__name" || params[type] === "labels__name")
|
||||
color = analytics?.extras?.colors.find((c) => c.name === value)?.color;
|
||||
|
||||
if (params[type] === "state__group") color = STATE_GROUP_COLORS[value];
|
||||
if (params[type] === "state__group") color = STATE_GROUP_COLORS[value.toLowerCase()];
|
||||
|
||||
if (params[type] === "priority")
|
||||
color =
|
||||
value === "urgent"
|
||||
value === "Urgent"
|
||||
? "#ef4444"
|
||||
: value === "high"
|
||||
: value === "High"
|
||||
? "#f97316"
|
||||
: value === "medium"
|
||||
: value === "Medium"
|
||||
? "#eab308"
|
||||
: value === "low"
|
||||
: value === "Low"
|
||||
? "#22c55e"
|
||||
: "#ced4da";
|
||||
|
||||
|
@ -11,16 +11,9 @@ import { ProjectSidebarList } from "components/project";
|
||||
export interface SidebarProps {
|
||||
toggleSidebar: boolean;
|
||||
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isAnalyticsModalOpen: boolean;
|
||||
setAnalyticsModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
toggleSidebar,
|
||||
setToggleSidebar,
|
||||
isAnalyticsModalOpen,
|
||||
setAnalyticsModal,
|
||||
}) => {
|
||||
const Sidebar: React.FC<SidebarProps> = ({ toggleSidebar, setToggleSidebar }) => {
|
||||
// theme
|
||||
const { collapsed: sidebarCollapse } = useTheme();
|
||||
|
||||
@ -34,10 +27,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
>
|
||||
<div className="flex h-full flex-1 flex-col">
|
||||
<WorkspaceSidebarDropdown />
|
||||
<WorkspaceSidebarMenu
|
||||
isAnalyticsModalOpen={isAnalyticsModalOpen}
|
||||
setAnalyticsModal={setAnalyticsModal}
|
||||
/>
|
||||
<WorkspaceSidebarMenu />
|
||||
<ProjectSidebarList />
|
||||
<WorkspaceHelpSection setSidebarActive={setToggleSidebar} />
|
||||
</div>
|
||||
|
@ -13,7 +13,6 @@ import AppHeader from "layouts/app-layout/app-header";
|
||||
import AppSidebar from "layouts/app-layout/app-sidebar";
|
||||
// components
|
||||
import { NotAuthorizedView, JoinProject } from "components/auth-screens";
|
||||
import { AnalyticsWorkspaceModal } from "components/analytics";
|
||||
import { CommandPalette } from "components/command-palette";
|
||||
// ui
|
||||
import { PrimaryButton, Spinner } from "components/ui";
|
||||
@ -53,13 +52,10 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
right,
|
||||
}) => {
|
||||
const [toggleSidebar, setToggleSidebar] = useState(false);
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { issueView } = useIssuesView();
|
||||
|
||||
const { loading, error, memberRole: memberType } = useProjectMyMembership();
|
||||
|
||||
const settingsLayout = router.pathname.includes("/settings");
|
||||
@ -68,12 +64,7 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
<Container meta={meta}>
|
||||
<CommandPalette />
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<AppSidebar
|
||||
toggleSidebar={toggleSidebar}
|
||||
setToggleSidebar={setToggleSidebar}
|
||||
isAnalyticsModalOpen={analyticsModal}
|
||||
setAnalyticsModal={setAnalyticsModal}
|
||||
/>
|
||||
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
||||
|
||||
{loading ? (
|
||||
<div className="grid h-full w-full place-items-center p-4">
|
||||
@ -121,12 +112,6 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
|
||||
: "bg-brand-base"
|
||||
}`}
|
||||
>
|
||||
{analyticsModal && (
|
||||
<AnalyticsWorkspaceModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
/>
|
||||
)}
|
||||
{!noHeader && (
|
||||
<AppHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
|
@ -14,7 +14,6 @@ import AppHeader from "layouts/app-layout/app-header";
|
||||
import { UserAuthorizationLayout } from "./user-authorization-wrapper";
|
||||
// components
|
||||
import { NotAuthorizedView, NotAWorkspaceMember } from "components/auth-screens";
|
||||
import { AnalyticsWorkspaceModal } from "components/analytics";
|
||||
import { CommandPalette } from "components/command-palette";
|
||||
// icons
|
||||
import { PrimaryButton, Spinner } from "components/ui";
|
||||
@ -49,7 +48,6 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
||||
right,
|
||||
}) => {
|
||||
const [toggleSidebar, setToggleSidebar] = useState(false);
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -93,12 +91,7 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
||||
<Container meta={meta}>
|
||||
<CommandPalette />
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<AppSidebar
|
||||
toggleSidebar={toggleSidebar}
|
||||
setToggleSidebar={setToggleSidebar}
|
||||
isAnalyticsModalOpen={analyticsModal}
|
||||
setAnalyticsModal={setAnalyticsModal}
|
||||
/>
|
||||
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
|
||||
{settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? (
|
||||
<NotAuthorizedView
|
||||
actionButton={
|
||||
@ -122,12 +115,6 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
|
||||
: "bg-brand-base"
|
||||
}`}
|
||||
>
|
||||
{analyticsModal && (
|
||||
<AnalyticsWorkspaceModal
|
||||
isOpen={analyticsModal}
|
||||
onClose={() => setAnalyticsModal(false)}
|
||||
/>
|
||||
)}
|
||||
{!noHeader && (
|
||||
<AppHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
|
108
apps/app/pages/[workspaceSlug]/analytics.tsx
Normal file
108
apps/app/pages/[workspaceSlug]/analytics.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
// services
|
||||
import analyticsService from "services/analytics.service";
|
||||
// layouts
|
||||
import { WorkspaceAuthorizationLayout } from "layouts/auth-layout";
|
||||
// components
|
||||
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import { IAnalyticsParams } from "types";
|
||||
// fetch-keys
|
||||
import { ANALYTICS } from "constants/fetch-keys";
|
||||
|
||||
const defaultValues: IAnalyticsParams = {
|
||||
x_axis: "priority",
|
||||
y_axis: "issue_count",
|
||||
segment: null,
|
||||
project: null,
|
||||
};
|
||||
|
||||
const tabsList = ["Scope and Demand", "Custom Analytics"];
|
||||
|
||||
const Analytics = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { control, watch, setValue } = useForm<IAnalyticsParams>({ defaultValues });
|
||||
|
||||
const params: IAnalyticsParams = {
|
||||
x_axis: watch("x_axis"),
|
||||
y_axis: watch("y_axis"),
|
||||
segment: watch("segment"),
|
||||
project: watch("project"),
|
||||
};
|
||||
|
||||
const { data: analytics, error: analyticsError } = useSWR(
|
||||
workspaceSlug ? ANALYTICS(workspaceSlug.toString(), params) : null,
|
||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceAuthorizationLayout
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Workspace Analytics" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
// right={
|
||||
// <PrimaryButton
|
||||
// className="flex items-center gap-2"
|
||||
// onClick={() => {
|
||||
// const e = new KeyboardEvent("keydown", { key: "p" });
|
||||
// document.dispatchEvent(e);
|
||||
// }}
|
||||
// >
|
||||
// <PlusIcon className="h-4 w-4" />
|
||||
// Save Analytics
|
||||
// </PrimaryButton>
|
||||
// }
|
||||
>
|
||||
<div className="h-full flex flex-col overflow-hidden bg-brand-base">
|
||||
<Tab.Group as={Fragment}>
|
||||
<Tab.List as="div" className="space-x-2 border-b border-brand-base px-5 py-3">
|
||||
{tabsList.map((tab) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
className={({ selected }) =>
|
||||
`rounded-3xl border border-brand-base px-4 py-2 text-xs hover:bg-brand-surface-2 ${
|
||||
selected ? "bg-brand-surface-2" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels as={Fragment}>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<ScopeAndDemand fullScreen />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel as={Fragment}>
|
||||
<CustomAnalytics
|
||||
analytics={analytics}
|
||||
analyticsError={analyticsError}
|
||||
params={params}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
fullScreen
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</WorkspaceAuthorizationLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics;
|
@ -18,7 +18,10 @@ class AnalyticsServices extends APIService {
|
||||
|
||||
async getAnalytics(workspaceSlug: string, params: IAnalyticsParams): Promise<IAnalyticsResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/analytics/`, {
|
||||
params,
|
||||
params: {
|
||||
...params,
|
||||
project: params?.project ? params.project.toString() : null,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
@ -31,7 +34,10 @@ class AnalyticsServices extends APIService {
|
||||
params?: Partial<IAnalyticsParams>
|
||||
): Promise<IDefaultAnalyticsResponse> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/default-analytics/`, {
|
||||
params,
|
||||
params: {
|
||||
...params,
|
||||
project: params?.project ? params.project.toString() : null,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
12
apps/app/types/analytics.d.ts
vendored
12
apps/app/types/analytics.d.ts
vendored
@ -11,7 +11,7 @@ export interface IAnalyticsData {
|
||||
dimension: string | null;
|
||||
segment?: string;
|
||||
count?: number;
|
||||
effort?: number | null;
|
||||
estimate?: number | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
@ -34,13 +34,13 @@ export type TXAxisValues =
|
||||
| "created_at"
|
||||
| "completed_at";
|
||||
|
||||
export type TYAxisValues = "issue_count" | "effort";
|
||||
export type TYAxisValues = "issue_count" | "estimate";
|
||||
|
||||
export interface IAnalyticsParams {
|
||||
x_axis: TXAxisValues;
|
||||
y_axis: TYAxisValues;
|
||||
segment?: TXAxisValues | null;
|
||||
project?: string | null;
|
||||
project?: string[] | null;
|
||||
cycle?: string | null;
|
||||
module?: string | null;
|
||||
}
|
||||
@ -59,7 +59,8 @@ export interface IExportAnalyticsFormData {
|
||||
|
||||
export interface IDefaultAnalyticsUser {
|
||||
assignees__avatar: string | null;
|
||||
assignees__email: string;
|
||||
assignees__first_name: string;
|
||||
assignees__last_name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
@ -68,7 +69,8 @@ export interface IDefaultAnalyticsResponse {
|
||||
most_issue_closed_user: IDefaultAnalyticsUser[];
|
||||
most_issue_created_user: {
|
||||
created_by__avatar: string | null;
|
||||
created_by__email: string;
|
||||
created_by__first_name: string;
|
||||
created_by__last_name: string;
|
||||
count: number;
|
||||
}[];
|
||||
open_estimate_sum: number;
|
||||
|
3
apps/app/types/projects.d.ts
vendored
3
apps/app/types/projects.d.ts
vendored
@ -27,6 +27,9 @@ export interface IProject {
|
||||
network: number;
|
||||
project_lead: IUser | string | null;
|
||||
slug: string;
|
||||
total_cycles: number;
|
||||
total_members: number;
|
||||
total_modules: number;
|
||||
updated_at: Date;
|
||||
updated_by: string;
|
||||
workspace: IWorkspace | string;
|
||||
|
Loading…
Reference in New Issue
Block a user