chore: x-axis tick values for assignees (#1060)

* style: new custom analytics ui

* fix: x-axis assignee tick values

* chore: assignee names in the custom analytics table
This commit is contained in:
Aaryan Khandelwal 2023-05-16 15:11:40 +05:30 committed by GitHub
parent c060f7db30
commit d575e8ec6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 233 additions and 140 deletions

View File

@ -68,7 +68,50 @@ export const AnalyticsGraph: React.FC<Props> = ({
customYAxisTickValues={generateYAxisTickValues()} customYAxisTickValues={generateYAxisTickValues()}
tooltip={(datum) => <CustomTooltip datum={datum} params={params} />} tooltip={(datum) => <CustomTooltip datum={datum} params={params} />}
height={fullScreen ? "400px" : "300px"} height={fullScreen ? "400px" : "300px"}
margin={{ right: 20, bottom: longestXAxisLabel.length * 5 + 20 }} margin={{
right: 20,
bottom: params.x_axis === "assignees__email" ? 50 : longestXAxisLabel.length * 5 + 20,
}}
axisBottom={{
tickSize: 0,
tickPadding: 10,
tickRotation: barGraphData.data.length > 7 ? -45 : 0,
renderTick:
params.x_axis === "assignees__email"
? (datum) => {
const avatar = analytics.extras.assignee_details?.find(
(a) => a.assignees__email === datum.value
)?.assignees__avatar;
console.log(avatar);
if (avatar && avatar !== "")
return (
<g transform={`translate(${datum.x},${datum.y})`}>
<image
x={-8}
y={10}
width={16}
height={16}
xlinkHref={avatar}
style={{ clipPath: "circle(50%)" }}
/>
</g>
);
else
return (
<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 && datum.value !== "None"
? `${datum.value}`.toUpperCase()[0]
: "?"}
</text>
</g>
);
}
: undefined,
}}
theme={{ theme={{
background: "rgb(var(--color-bg-base))", background: "rgb(var(--color-bg-base))",
axis: {}, axis: {},

View File

@ -23,9 +23,9 @@ export const AnalyticsSelectBar: React.FC<Props> = ({
isProjectLevel, isProjectLevel,
}) => ( }) => (
<div <div
className={`grid items-center gap-4 p-5 pb-0.5 ${ className={`grid items-center gap-4 px-5 py-2.5 ${
isProjectLevel ? "grid-cols-3" : "grid-cols-2" isProjectLevel ? "grid-cols-3" : "grid-cols-2"
} ${fullScreen ? "lg:grid-cols-4" : ""}`} } ${fullScreen ? "lg:grid-cols-4 md:py-5" : ""}`}
> >
{!isProjectLevel && ( {!isProjectLevel && (
<div> <div>

View File

@ -108,9 +108,9 @@ export const AnalyticsSidebar: React.FC<Props> = ({
return ( return (
<div <div
className={`p-5 pb-0 flex flex-col space-y-2 md:space-y-4 overflow-hidden ${ className={`p-5 pb-0 flex flex-col space-y-2 ${
fullScreen fullScreen
? "pb-5 border-l border-brand-base md:h-full md:pb-5 md:border-l md:border-brand-base" ? "pb-5 border-l border-brand-base md:h-full md:pb-5 md:border-l md:border-brand-base md:space-y-4 overflow-hidden"
: "" : ""
}`} }`}
> >
@ -176,7 +176,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
{projectId ? ( {projectId ? (
cycleId && cycleDetails ? ( cycleId && cycleDetails ? (
<div className="hidden md:block h-full overflow-y-auto"> <div className="hidden md:block h-full overflow-y-auto">
<h4 className="font-medium break-all">{cycleDetails.name}</h4> <h4 className="font-medium break-all">Analytics for {cycleDetails.name}</h4>
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Lead</h6> <h6 className="text-brand-secondary">Lead</h6>
@ -204,7 +204,7 @@ export const AnalyticsSidebar: React.FC<Props> = ({
</div> </div>
) : moduleId && moduleDetails ? ( ) : moduleId && moduleDetails ? (
<div className="hidden md:block h-full overflow-y-auto"> <div className="hidden md:block h-full overflow-y-auto">
<h4 className="font-medium break-all">{moduleDetails.name}</h4> <h4 className="font-medium break-all">Analytics for {moduleDetails.name}</h4>
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<h6 className="text-brand-secondary">Lead</h6> <h6 className="text-brand-secondary">Lead</h6>

View File

@ -21,96 +21,115 @@ type Props = {
yAxisKey: "count" | "estimate"; yAxisKey: "count" | "estimate";
}; };
export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => ( export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, params, yAxisKey }) => {
<div className="flow-root"> const renderAssigneeName = (email: string): string => {
<div className="overflow-x-auto"> const assignee = analytics.extras.assignee_details.find((a) => a.assignees__email === email);
<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"> if (!assignee) return "No assignee";
<thead className="bg-brand-surface-2">
<tr className="divide-x divide-brand-base text-sm text-brand-base"> if (assignee.assignees__first_name !== "")
<th scope="col" className="py-3 px-2.5 text-left font-medium"> return assignee.assignees__first_name + " " + assignee.assignees__last_name;
{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === params.x_axis)?.label}
</th> return email;
{params.segment ? ( };
barGraphData.xAxisKeys.map((key) => (
<th return (
key={`segment-${key}`} <div className="flow-root">
scope="col" <div className="overflow-x-auto">
className={`px-2.5 py-3 text-left font-medium ${ <div className="inline-block min-w-full align-middle">
params.segment === "priority" || params.segment === "state__group" <table className="min-w-full divide-y divide-brand-base whitespace-nowrap border-y border-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}
</th>
{params.segment ? (
barGraphData.xAxisKeys.map((key) => (
<th
key={`segment-${key}`}
scope="col"
className={`px-2.5 py-3 text-left font-medium ${
params.segment === "priority" || params.segment === "state__group"
? "capitalize"
: ""
}`}
>
<div className="flex items-center gap-2">
{params.segment === "priority" ? (
getPriorityIcon(key)
) : (
<span
className="h-3 w-3 flex-shrink-0 rounded"
style={{
backgroundColor: generateBarColor(key, analytics, params, "segment"),
}}
/>
)}
{DATE_KEYS.includes(params.segment ?? "")
? renderMonthAndYear(key)
: params.segment === "assignees__email"
? renderAssigneeName(key)
: key}
</div>
</th>
))
) : (
<th scope="col" className="py-3 px-2.5 text-left font-medium sm:pr-0">
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label}
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-brand-base">
{barGraphData.data.map((item, index) => (
<tr
key={`table-row-${index}`}
className="divide-x divide-brand-base text-xs text-brand-secondary"
>
<td
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
params.x_axis === "priority" || params.x_axis === "state__group"
? "capitalize" ? "capitalize"
: "" : ""
}`} }`}
> >
<div className="flex items-center gap-2"> {params.x_axis === "priority" ? (
{params.segment === "priority" ? ( getPriorityIcon(`${item.name}`)
getPriorityIcon(key) ) : (
) : ( <span
<span className="h-3 w-3 rounded"
className="h-3 w-3 flex-shrink-0 rounded" style={{
style={{ backgroundColor: generateBarColor(
backgroundColor: generateBarColor(key, analytics, params, "segment"), `${item.name}`,
}} analytics,
/> params,
)} "x_axis"
{DATE_KEYS.includes(params.segment ?? "") ? renderMonthAndYear(key) : key} ),
</div> }}
</th> />
)) )}
) : ( {params.x_axis === "assignees__email"
<th scope="col" className="py-3 px-2.5 text-left font-medium sm:pr-0"> ? renderAssigneeName(`${item.name}`)
{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === params.y_axis)?.label} : addSpaceIfCamelCase(`${item.name}`)}
</th> </td>
)} {params.segment ? (
</tr> barGraphData.xAxisKeys.map((key, index) => (
</thead> <td
<tbody className="divide-y divide-brand-base"> key={`segment-value-${index}`}
{barGraphData.data.map((item, index) => ( className="whitespace-nowrap py-2 px-2.5 sm:pr-0"
<tr >
key={`table-row-${index}`} {item[key] ?? 0}
className="divide-x divide-brand-base text-xs text-brand-secondary" </td>
> ))
<td
className={`flex items-center gap-2 whitespace-nowrap py-2 px-2.5 font-medium ${
params.x_axis === "priority" || params.x_axis === "state__group"
? "capitalize"
: ""
}`}
>
{params.x_axis === "priority" ? (
getPriorityIcon(`${item.name}`)
) : ( ) : (
<span <td className="whitespace-nowrap py-2 px-2.5 sm:pr-0">{item[yAxisKey]}</td>
className="h-3 w-3 rounded"
style={{
backgroundColor: generateBarColor(
`${item.name}`,
analytics,
params,
"x_axis"
),
}}
/>
)} )}
{addSpaceIfCamelCase(`${item.name}`)} </tr>
</td> ))}
{params.segment ? ( </tbody>
barGraphData.xAxisKeys.map((key, index) => ( </table>
<td </div>
key={`segment-value-${index}`}
className="whitespace-nowrap py-2 px-2.5 sm:pr-0"
>
{item[key] ?? 0}
</td>
))
) : (
<td className="whitespace-nowrap py-2 px-2.5 sm:pr-0">{item[yAxisKey]}</td>
)}
</tr>
))}
</tbody>
</table>
</div> </div>
</div> </div>
</div> );
); };

View File

@ -37,7 +37,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
</div> </div>
<p className="text-brand-secondary">{percentage}%</p> <p className="text-brand-secondary">{percentage}%</p>
</div> </div>
<div className="bar relative h-1 w-full rounded bg-brand-base"> <div className="bar relative h-1 w-full rounded bg-brand-surface-2">
<div <div
className="absolute top-0 left-0 h-1 rounded duration-300" className="absolute top-0 left-0 h-1 rounded duration-300"
style={{ style={{

View File

@ -3,6 +3,7 @@ import Image from "next/image";
type Props = { type Props = {
users: { users: {
avatar: string | null; avatar: string | null;
email: string | null;
firstName: string; firstName: string;
lastName: string; lastName: string;
count: number; count: number;
@ -14,8 +15,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, index) => ( {users.map((user) => (
<div key={`user-${index}`} className="flex items-start justify-between gap-4 text-xs"> <div key={user.email ?? "None"} 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">
@ -24,7 +25,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
height="100%" height="100%"
width="100%" width="100%"
className="rounded-full" className="rounded-full"
alt={user.firstName + " " + user.lastName} alt={user.email ?? "None"}
/> />
</div> </div>
) : ( ) : (

View File

@ -56,6 +56,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
<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, firstName: user.created_by__first_name,
lastName: user.created_by__last_name, lastName: user.created_by__last_name,
count: user.count, count: user.count,
@ -65,6 +66,7 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
<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, firstName: user.assignees__first_name,
lastName: user.assignees__last_name, lastName: user.assignees__last_name,
count: user.count, count: user.count,

View File

@ -30,7 +30,6 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
: "All projects" : "All projects"
} }
optionsClassName="min-w-full" optionsClassName="min-w-full"
position="right"
noChevron noChevron
multiple multiple
/> />

View File

@ -1,3 +1,5 @@
import { useRouter } from "next/router";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// types // types
@ -11,29 +13,36 @@ type Props = {
params: IAnalyticsParams; params: IAnalyticsParams;
}; };
export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => ( export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => {
<CustomSelect const router = useRouter();
value={value} const { cycleId, moduleId } = router.query;
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}> <CustomSelect
{item.label} value={value}
</CustomSelect.Option> label={
); <span>
})} {ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label ?? (
</CustomSelect> <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;
if (cycleId && item.value === "issue_cycle__cycle__name") return null;
if (moduleId && item.value === "issue_module__module__name") return null;
return (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
);
})}
</CustomSelect>
);
};

View File

@ -1,27 +1,39 @@
import { useRouter } from "next/router";
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// types // types
import { IAnalyticsParams, TXAxisValues, TYAxisValues } from "types"; import { TXAxisValues } from "types";
// constants // constants
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";
type Props = { type Props = {
value: TXAxisValues; value: TXAxisValues;
onChange: (val: string) => void; onChange: (val: string) => void;
}; };
export const SelectXAxis: React.FC<Props> = ({ value, onChange }) => ( export const SelectXAxis: React.FC<Props> = ({ value, onChange }) => {
<CustomSelect const router = useRouter();
value={value} const { cycleId, moduleId } = router.query;
label={<span>{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label}</span>}
onChange={onChange} return (
width="w-full" <CustomSelect
maxHeight="lg" value={value}
> label={<span>{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label}</span>}
{ANALYTICS_X_AXIS_VALUES.map((item) => ( onChange={onChange}
<CustomSelect.Option key={item.value} value={item.value}> width="w-full"
{item.label} maxHeight="lg"
</CustomSelect.Option> >
))} {ANALYTICS_X_AXIS_VALUES.map((item) => {
</CustomSelect> if (cycleId && item.value === "issue_cycle__cycle__name") return null;
); if (moduleId && item.value === "issue_module__module__name") return null;
return (
<CustomSelect.Option key={item.value} value={item.value}>
{item.label}
</CustomSelect.Option>
);
})}
</CustomSelect>
);
};

View File

@ -1,7 +1,7 @@
// ui // ui
import { CustomSelect } from "components/ui"; import { CustomSelect } from "components/ui";
// types // types
import { IAnalyticsParams, TYAxisValues } from "types"; import { TYAxisValues } from "types";
// constants // constants
import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";

View File

@ -3,6 +3,12 @@ export interface IAnalyticsResponse {
distribution: IAnalyticsData; distribution: IAnalyticsData;
extras: { extras: {
colors: IAnalyticsExtra[]; colors: IAnalyticsExtra[];
assignee_details: {
assignees__avatar: string | null;
assignees__email: string;
assignees__first_name: string;
assignees__last_name: string;
}[];
}; };
} }
@ -59,6 +65,7 @@ export interface IExportAnalyticsFormData {
export interface IDefaultAnalyticsUser { export interface IDefaultAnalyticsUser {
assignees__avatar: string | null; assignees__avatar: string | null;
assignees__email: string | null;
assignees__first_name: string; assignees__first_name: string;
assignees__last_name: string; assignees__last_name: string;
count: number; count: number;
@ -69,6 +76,7 @@ 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 | null;
created_by__first_name: string; created_by__first_name: string;
created_by__last_name: string; created_by__last_name: string;
count: number; count: number;