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