fix: workspace and analytics page casing

This commit is contained in:
Aaryan Khandelwal 2023-11-28 22:30:07 +05:30
parent 3400c119bc
commit aae54fb69f
11 changed files with 216 additions and 214 deletions

View File

@ -15,7 +15,7 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = (props) => {
return ( return (
<div className="hidden h-full overflow-hidden md:flex md:flex-col"> <div className="hidden h-full overflow-hidden md:flex md:flex-col">
<h4 className="font-medium">Selected Projects</h4> <h4 className="font-medium">Selected projects</h4>
<div className="space-y-6 mt-4 h-full overflow-y-auto"> <div className="space-y-6 mt-4 h-full overflow-y-auto">
{projects.map((project) => ( {projects.map((project) => (
<div key={project.id} className="w-full"> <div key={project.id} className="w-full">

View File

@ -29,172 +29,172 @@ type Props = {
const analyticsService = new AnalyticsService(); const analyticsService = new AnalyticsService();
export const CustomAnalyticsSidebar: React.FC<Props> = observer( export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
({ analytics, params, fullScreen, isProjectLevel = false }) => { const { analytics, params, fullScreen, isProjectLevel = false } = props;
const router = useRouter(); // router
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
// toast
const { setToastAlert } = useToast();
// mobx store
const {
project: { workspaceProjects, getProjectById },
cycle: { getCycleById, fetchCycleWithId },
module: { getModuleById, fetchModuleDetails },
} = useMobxStore();
const { setToastAlert } = useToast(); const projectDetails =
workspaceSlug && projectId
? getProjectById(workspaceSlug.toString(), projectId.toString()) ?? undefined
: undefined;
const { user: userStore, project: projectStore, cycle: cycleStore, module: moduleStore } = useMobxStore(); const trackExportAnalytics = () => {
const eventPayload: any = {
const user = userStore.currentUser; workspaceSlug: workspaceSlug?.toString(),
params: {
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
const projectDetails =
workspaceSlug && projectId
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) ?? undefined
: undefined;
const trackExportAnalytics = () => {
if (!user) return;
const eventPayload: any = {
workspaceSlug: workspaceSlug?.toString(),
params: {
x_axis: params.x_axis,
y_axis: params.y_axis,
group: params.segment,
project: params.project,
},
};
if (projectDetails) {
const workspaceDetails = projectDetails.workspace as IWorkspace;
eventPayload.workspaceId = workspaceDetails.id;
eventPayload.workspaceName = workspaceDetails.name;
eventPayload.projectId = projectDetails.id;
eventPayload.projectIdentifier = projectDetails.identifier;
eventPayload.projectName = projectDetails.name;
}
if (cycleDetails || moduleDetails) {
const details = cycleDetails || moduleDetails;
eventPayload.workspaceId = details?.workspace_detail?.id;
eventPayload.workspaceName = details?.workspace_detail?.name;
eventPayload.projectId = details?.project_detail.id;
eventPayload.projectIdentifier = details?.project_detail.identifier;
eventPayload.projectName = details?.project_detail.name;
}
if (cycleDetails) {
eventPayload.cycleId = cycleDetails.id;
eventPayload.cycleName = cycleDetails.name;
}
if (moduleDetails) {
eventPayload.moduleId = moduleDetails.id;
eventPayload.moduleName = moduleDetails.name;
}
};
const exportAnalytics = () => {
if (!workspaceSlug) return;
const data: IExportAnalyticsFormData = {
x_axis: params.x_axis, x_axis: params.x_axis,
y_axis: params.y_axis, y_axis: params.y_axis,
}; group: params.segment,
project: params.project,
if (params.segment) data.segment = params.segment; },
if (params.project) data.project = params.project;
analyticsService
.exportAnalytics(workspaceSlug.toString(), data)
.then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: res.message,
});
trackExportAnalytics();
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "There was some error in exporting the analytics. Please try again.",
})
);
}; };
const cycleDetails = cycleId ? cycleStore.getCycleById(cycleId.toString()) : undefined; if (projectDetails) {
const moduleDetails = moduleId ? moduleStore.getModuleById(moduleId.toString()) : undefined; const workspaceDetails = projectDetails.workspace as IWorkspace;
// fetch cycle details eventPayload.workspaceId = workspaceDetails.id;
useEffect(() => { eventPayload.workspaceName = workspaceDetails.name;
if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return; eventPayload.projectId = projectDetails.id;
eventPayload.projectIdentifier = projectDetails.identifier;
eventPayload.projectName = projectDetails.name;
}
cycleStore.fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); if (cycleDetails || moduleDetails) {
}, [cycleId, cycleDetails, cycleStore, projectId, workspaceSlug]); const details = cycleDetails || moduleDetails;
// fetch module details eventPayload.workspaceId = details?.workspace_detail?.id;
useEffect(() => { eventPayload.workspaceName = details?.workspace_detail?.name;
if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return; eventPayload.projectId = details?.project_detail.id;
eventPayload.projectIdentifier = details?.project_detail.identifier;
eventPayload.projectName = details?.project_detail.name;
}
moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); if (cycleDetails) {
}, [moduleId, moduleDetails, moduleStore, projectId, workspaceSlug]); eventPayload.cycleId = cycleDetails.id;
eventPayload.cycleName = cycleDetails.name;
}
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id); if (moduleDetails) {
eventPayload.moduleId = moduleDetails.id;
eventPayload.moduleName = moduleDetails.name;
}
};
return ( const exportAnalytics = () => {
<div if (!workspaceSlug) return;
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
fullScreen const data: IExportAnalyticsFormData = {
? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5" x_axis: params.x_axis,
: "" y_axis: params.y_axis,
}`} };
>
<div className="flex items-center gap-2 flex-wrap"> if (params.segment) data.segment = params.segment;
if (params.project) data.project = params.project;
analyticsService
.exportAnalytics(workspaceSlug.toString(), data)
.then((res) => {
setToastAlert({
type: "success",
title: "Success!",
message: res.message,
});
trackExportAnalytics();
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "There was some error in exporting the analytics. Please try again.",
})
);
};
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
// fetch cycle details
useEffect(() => {
if (!workspaceSlug || !projectId || !cycleId || cycleDetails) return;
fetchCycleWithId(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
}, [cycleId, cycleDetails, fetchCycleWithId, projectId, workspaceSlug]);
// fetch module details
useEffect(() => {
if (!workspaceSlug || !projectId || !moduleId || moduleDetails) return;
fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
}, [moduleId, moduleDetails, fetchModuleDetails, projectId, workspaceSlug]);
const selectedProjects =
params.project && params.project.length > 0 ? params.project : workspaceProjects?.map((p) => p.id);
return (
<div
className={`px-5 py-2.5 flex items-center justify-between space-y-2 ${
fullScreen
? "border-l border-custom-border-200 md:h-full md:border-l md:border-custom-border-200 md:space-y-4 overflow-hidden md:flex-col md:items-start md:py-5"
: ""
}`}
>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
<LayersIcon height={14} width={14} />
{analytics ? analytics.total : "..."} Issues
</div>
{isProjectLevel && (
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs"> <div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
<LayersIcon height={14} width={14} /> <CalendarDays className="h-3.5 w-3.5" />
{analytics ? analytics.total : "..."} Issues {renderShortDate(
(cycleId
? cycleDetails?.created_at
: moduleId
? moduleDetails?.created_at
: projectDetails?.created_at) ?? ""
)}
</div> </div>
{isProjectLevel && ( )}
<div className="flex items-center gap-1 bg-custom-background-80 rounded-md px-3 py-1 text-custom-text-200 text-xs">
<CalendarDays className="h-3.5 w-3.5" />
{renderShortDate(
(cycleId
? cycleDetails?.created_at
: moduleId
? moduleDetails?.created_at
: projectDetails?.created_at) ?? ""
)}
</div>
)}
</div>
<div className="h-full w-full overflow-hidden">
{fullScreen ? (
<>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<CustomAnalyticsSidebarProjectsList
projects={projects?.filter((p) => selectedProjects.includes(p.id)) ?? []}
/>
)}
<CustomAnalyticsSidebarHeader />
</>
) : null}
</div>
<div className="flex items-center gap-2 flex-wrap justify-self-end">
<Button
variant="neutral-primary"
prependIcon={<RefreshCw className="h-3.5 w-3.5" />}
onClick={() => {
if (!workspaceSlug) return;
mutate(ANALYTICS(workspaceSlug.toString(), params));
}}
>
Refresh
</Button>
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
Export as CSV
</Button>
</div>
</div> </div>
); <div className="h-full w-full overflow-hidden">
} {fullScreen ? (
); <>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<CustomAnalyticsSidebarProjectsList
projects={workspaceProjects?.filter((p) => selectedProjects.includes(p.id)) ?? []}
/>
)}
<CustomAnalyticsSidebarHeader />
</>
) : null}
</div>
<div className="flex items-center gap-2 flex-wrap justify-self-end">
<Button
variant="neutral-primary"
prependIcon={<RefreshCw className="h-3.5 w-3.5" />}
onClick={() => {
if (!workspaceSlug) return;
mutate(ANALYTICS(workspaceSlug.toString(), params));
}}
>
Refresh
</Button>
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
Export as CSV
</Button>
</div>
</div>
);
});

View File

@ -53,7 +53,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs"> <div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
<p className="flex items-center gap-1 text-custom-text-200"> <p className="flex items-center gap-1 text-custom-text-200">
<Triangle className="h-4 w-4" /> <Triangle className="h-4 w-4" />
<span>Estimate Demand:</span> <span>Estimate demand:</span>
</p> </p>
<p className="font-medium"> <p className="font-medium">
{defaultAnalytics.open_estimate_sum}/{defaultAnalytics.total_estimate_sum} {defaultAnalytics.open_estimate_sum}/{defaultAnalytics.total_estimate_sum}

View File

@ -17,45 +17,49 @@ type Props = {
workspaceSlug: string; workspaceSlug: string;
}; };
export const AnalyticsLeaderBoard: React.FC<Props> = ({ users, title, emptyStateMessage, workspaceSlug }) => ( export const AnalyticsLeaderBoard: React.FC<Props> = (props) => {
<div className="p-3 border border-custom-border-200 rounded-[10px]"> const { users, title, emptyStateMessage, workspaceSlug } = props;
<h6 className="text-base font-medium">{title}</h6>
{users.length > 0 ? ( return (
<div className="mt-3 space-y-3"> <div className="p-3 border border-custom-border-200 rounded-[10px]">
{users.map((user) => ( <h6 className="text-base font-medium">{title}</h6>
<a {users.length > 0 ? (
key={user.display_name ?? "None"} <div className="mt-3 space-y-3">
href={`/${workspaceSlug}/profile/${user.id}`} {users.map((user) => (
target="_blank" <a
rel="noopener noreferrer" key={user.display_name ?? "None"}
className="flex items-start justify-between gap-4 text-xs" href={`/${workspaceSlug}/profile/${user.id}`}
> target="_blank"
<div className="flex items-center gap-2"> rel="noopener noreferrer"
{user && user.avatar && user.avatar !== "" ? ( className="flex items-start justify-between gap-4 text-xs"
<div className="relative rounded-full h-4 w-4 flex-shrink-0"> >
<img <div className="flex items-center gap-2">
src={user.avatar} {user && user.avatar && user.avatar !== "" ? (
className="absolute top-0 left-0 h-full w-full object-cover rounded-full" <div className="relative rounded-full h-4 w-4 flex-shrink-0">
alt={user.display_name ?? "None"} <img
/> src={user.avatar}
</div> className="absolute top-0 left-0 h-full w-full object-cover rounded-full"
) : ( alt={user.display_name ?? "None"}
<div className="grid place-items-center flex-shrink-0 rounded-full bg-gray-700 text-[11px] capitalize text-white h-4 w-4"> />
{user.display_name !== "" ? user?.display_name?.[0] : "?"} </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">
<span className="break-words text-custom-text-200"> {user.display_name !== "" ? user?.display_name?.[0] : "?"}
{user.display_name !== "" ? `${user.display_name}` : "No assignee"} </div>
</span> )}
</div> <span className="break-words text-custom-text-200">
<span className="flex-shrink-0">{user.count}</span> {user.display_name !== "" ? `${user.display_name}` : "No assignee"}
</a> </span>
))} </div>
</div> <span className="flex-shrink-0">{user.count}</span>
) : ( </a>
<div className="px-7 py-4"> ))}
<ProfileEmptyState title="No Data yet" description={emptyStateMessage} image={emptyUsers} /> </div>
</div> ) : (
)} <div className="px-7 py-4">
</div> <ProfileEmptyState title="No data yet" description={emptyStateMessage} image={emptyUsers} />
); </div>
)}
</div>
);
};

View File

@ -2,17 +2,16 @@ import { useState } from "react";
import { LayoutGrid, Zap } from "lucide-react"; import { LayoutGrid, Zap } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// images
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
// components // components
import { ProductUpdatesModal } from "components/common"; import { ProductUpdatesModal } from "components/common";
// ui
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs } from "@plane/ui";
import { useMobxStore } from "lib/mobx/store-provider"; // assetsˀ
import githubBlackImage from "/public/logos/github-black.png";
import githubWhiteImage from "/public/logos/github-white.png";
export const WorkspaceDashboardHeader = () => { export const WorkspaceDashboardHeader = () => {
const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false); const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false);
const { trackEvent: { postHogEventTracker } } = useMobxStore();
// theme // theme
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
@ -39,7 +38,7 @@ export const WorkspaceDashboardHeader = () => {
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded flex-shrink-0" className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded flex-shrink-0"
> >
<Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" /> <Zap size={14} strokeWidth={2} fill="rgb(var(--color-text-100))" />
{"What's New?"} What{"'"}s new?
</a> </a>
<a <a
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded flex-shrink-0" className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded flex-shrink-0"

View File

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
@ -9,9 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { TourRoot } from "components/onboarding"; import { TourRoot } from "components/onboarding";
import { UserGreetingsView } from "components/user"; import { UserGreetingsView } from "components/user";
import { CompletedIssuesGraph, IssuesList, IssuesPieChart, IssuesStats } from "components/workspace"; import { CompletedIssuesGraph, IssuesList, IssuesPieChart, IssuesStats } from "components/workspace";
import { Button } from "@plane/ui";
// images // images
import emptyDashboard from "public/empty-state/dashboard.svg";
import { NewEmptyState } from "components/common/new-empty-state"; import { NewEmptyState } from "components/common/new-empty-state";
import emptyProject from "public/empty-state/dashboard_empty_project.webp"; import emptyProject from "public/empty-state/dashboard_empty_project.webp";

View File

@ -34,7 +34,9 @@ export const IssuesList: React.FC<Props> = ({ issues, type }) => {
return ( return (
<div> <div>
<h3 className="mb-2 font-semibold capitalize">{type} Issues</h3> <h3 className="mb-2 font-semibold">
<span className="capitalize">{type}</span> issues
</h3>
{issues ? ( {issues ? (
<div className="h-[calc(100%-2.25rem)] rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 text-sm"> <div className="h-[calc(100%-2.25rem)] rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 text-sm">
<div <div
@ -44,7 +46,7 @@ export const IssuesList: React.FC<Props> = ({ issues, type }) => {
> >
<h4 className="capitalize">{type}</h4> <h4 className="capitalize">{type}</h4>
<h4 className="col-span-2">Issue</h4> <h4 className="col-span-2">Issue</h4>
<h4>{type === "overdue" ? "Due" : "Start"} Date</h4> <h4>{type === "overdue" ? "Due" : "Start"} date</h4>
</div> </div>
<div className="max-h-72 overflow-y-scroll"> <div className="max-h-72 overflow-y-scroll">
{issues.length > 0 ? ( {issues.length > 0 ? (

View File

@ -11,7 +11,7 @@ type Props = {
export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => ( export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
<div> <div>
<h3 className="mb-2 font-semibold">Issues by States</h3> <h3 className="mb-2 font-semibold">Issues by states</h3>
<div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4"> <div className="rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4">
<div className="grid grid-cols-1 sm:grid-cols-4"> <div className="grid grid-cols-1 sm:grid-cols-4">
<div className="sm:col-span-3"> <div className="sm:col-span-3">

View File

@ -15,6 +15,7 @@ type Props = {
export const IssuesStats: React.FC<Props> = ({ data }) => { export const IssuesStats: React.FC<Props> = ({ data }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
return ( return (
<div className="grid grid-cols-1 rounded-[10px] border border-custom-border-200 bg-custom-background-100 lg:grid-cols-3"> <div className="grid grid-cols-1 rounded-[10px] border border-custom-border-200 bg-custom-background-100 lg:grid-cols-3">
<div className="grid grid-cols-1 divide-y divide-custom-border-200 border-b border-custom-border-200 lg:border-r lg:border-b-0"> <div className="grid grid-cols-1 divide-y divide-custom-border-200 border-b border-custom-border-200 lg:border-r lg:border-b-0">
@ -77,8 +78,8 @@ export const IssuesStats: React.FC<Props> = ({ data }) => {
</div> </div>
</div> </div>
<div className="p-4 lg:col-span-2"> <div className="p-4 lg:col-span-2">
<h3 className="mb-2 font-semibold capitalize flex items-center gap-2"> <h3 className="mb-2 font-semibold flex items-center gap-2">
Activity Graph Activity graph
<Tooltip <Tooltip
tooltipContent="Your profile activity graph is a record of actions you've performed on issues across the workspace." tooltipContent="Your profile activity graph is a record of actions you've performed on issues across the workspace."
className="w-72 border border-custom-border-200" className="w-72 border border-custom-border-200"

View File

@ -60,7 +60,7 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] = [ export const ANALYTICS_Y_AXIS_VALUES: { value: TYAxisValues; label: string }[] = [
{ {
value: "issue_count", value: "issue_count",
label: "Issue Count", label: "Issue count",
}, },
{ {
value: "estimate", value: "estimate",

View File

@ -17,7 +17,6 @@ import emptyAnalytics from "public/empty-state/analytics.svg";
import { ANALYTICS_TABS } from "constants/analytics"; import { ANALYTICS_TABS } from "constants/analytics";
// type // type
import { NextPageWithLayout } from "types/app"; import { NextPageWithLayout } from "types/app";
import { NewEmptyState } from "components/common/new-empty-state";
const AnalyticsPage: NextPageWithLayout = observer(() => { const AnalyticsPage: NextPageWithLayout = observer(() => {
// store // store