style: new custom analytics ui (#1055)

This commit is contained in:
Aaryan Khandelwal 2023-05-16 10:41:37 +05:30 committed by GitHub
parent 8c707cc544
commit c6d78b5e6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 797 additions and 496 deletions

View File

@ -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 ?? [],
},
};

View File

@ -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>
);
};

View File

@ -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={{

View File

@ -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: {},
}}
/>

View File

@ -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";

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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}

View File

@ -1,4 +1,4 @@
export * from "./custom-analytics";
export * from "./scope-and-demand";
export * from "./select";
export * from "./project-modal";
export * from "./workspace-modal";

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
export * from "./project";
export * from "./segment";
export * from "./x-axis";
export * from "./y-axis";

View 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
/>
);
};

View 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>
);

View 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>
);

View 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>
);

View File

@ -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>
</>
);
};

View File

@ -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>;

View File

@ -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>
);
};

View File

@ -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",
},
];

View File

@ -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}`;

View File

@ -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";

View File

@ -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>

View File

@ -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}

View File

@ -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}

View 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;

View File

@ -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) => {

View File

@ -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;

View File

@ -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;