mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
c060f7db30
commit
d575e8ec6b
@ -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: {},
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -21,7 +21,19 @@ 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 }) => {
|
||||||
|
const renderAssigneeName = (email: string): string => {
|
||||||
|
const assignee = analytics.extras.assignee_details.find((a) => a.assignees__email === email);
|
||||||
|
|
||||||
|
if (!assignee) return "No assignee";
|
||||||
|
|
||||||
|
if (assignee.assignees__first_name !== "")
|
||||||
|
return assignee.assignees__first_name + " " + assignee.assignees__last_name;
|
||||||
|
|
||||||
|
return email;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="flow-root">
|
<div className="flow-root">
|
||||||
<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">
|
||||||
@ -53,7 +65,11 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{DATE_KEYS.includes(params.segment ?? "") ? renderMonthAndYear(key) : key}
|
{DATE_KEYS.includes(params.segment ?? "")
|
||||||
|
? renderMonthAndYear(key)
|
||||||
|
: params.segment === "assignees__email"
|
||||||
|
? renderAssigneeName(key)
|
||||||
|
: key}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
))
|
))
|
||||||
@ -92,7 +108,9 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{addSpaceIfCamelCase(`${item.name}`)}
|
{params.x_axis === "assignees__email"
|
||||||
|
? renderAssigneeName(`${item.name}`)
|
||||||
|
: addSpaceIfCamelCase(`${item.name}`)}
|
||||||
</td>
|
</td>
|
||||||
{params.segment ? (
|
{params.segment ? (
|
||||||
barGraphData.xAxisKeys.map((key, index) => (
|
barGraphData.xAxisKeys.map((key, index) => (
|
||||||
@ -114,3 +132,4 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -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={{
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
import { CustomSelect } from "components/ui";
|
import { CustomSelect } from "components/ui";
|
||||||
// types
|
// types
|
||||||
@ -11,7 +13,11 @@ type Props = {
|
|||||||
params: IAnalyticsParams;
|
params: IAnalyticsParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => (
|
export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
return (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={value}
|
value={value}
|
||||||
label={
|
label={
|
||||||
@ -28,6 +34,8 @@ export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => (
|
|||||||
<CustomSelect.Option value={null}>No value</CustomSelect.Option>
|
<CustomSelect.Option value={null}>No value</CustomSelect.Option>
|
||||||
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
||||||
if (params.x_axis === item.value) return null;
|
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 (
|
return (
|
||||||
<CustomSelect.Option key={item.value} value={item.value}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
@ -37,3 +45,4 @@ export const SelectSegment: React.FC<Props> = ({ value, onChange, params }) => (
|
|||||||
})}
|
})}
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
|
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 }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
return (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={value}
|
value={value}
|
||||||
label={<span>{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label}</span>}
|
label={<span>{ANALYTICS_X_AXIS_VALUES.find((v) => v.value === value)?.label}</span>}
|
||||||
@ -18,10 +24,16 @@ export const SelectXAxis: React.FC<Props> = ({ value, onChange }) => (
|
|||||||
width="w-full"
|
width="w-full"
|
||||||
maxHeight="lg"
|
maxHeight="lg"
|
||||||
>
|
>
|
||||||
{ANALYTICS_X_AXIS_VALUES.map((item) => (
|
{ANALYTICS_X_AXIS_VALUES.map((item) => {
|
||||||
|
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}>
|
<CustomSelect.Option key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
8
apps/app/types/analytics.d.ts
vendored
8
apps/app/types/analytics.d.ts
vendored
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user