style: responsive analytics (#3604)

This commit is contained in:
Ramesh Kumar Chandra 2024-02-09 15:52:25 +05:30 committed by GitHub
parent 2e129682b7
commit 3a14f19c99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 118 additions and 64 deletions

View File

@ -10,6 +10,8 @@ import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSi
import { IAnalyticsParams } from "@plane/types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
import { cn } from "helpers/common.helper";
import { useApplication } from "hooks/store";
type Props = {
additionalParams?: Partial<IAnalyticsParams>;
@ -46,11 +48,13 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
);
const { theme: themeStore } = useApplication();
const isProjectLevel = projectId ? true : false;
return (
<div className={`flex flex-col-reverse overflow-hidden ${fullScreen ? "md:grid md:h-full md:grid-cols-4" : ""}`}>
<div className="col-span-3 flex h-full flex-col overflow-hidden">
<div className={cn("relative w-full h-full flex overflow-hidden", isProjectLevel ? "flex-col-reverse" : "")}>
<div className="w-full flex h-full flex-col overflow-hidden">
<CustomAnalyticsSelectBar
control={control}
setValue={setValue}
@ -61,16 +65,22 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
<CustomAnalyticsMainContent
analytics={analytics}
error={analyticsError}
fullScreen={fullScreen}
params={params}
fullScreen={fullScreen}
/>
</div>
<CustomAnalyticsSidebar
analytics={analytics}
params={params}
fullScreen={fullScreen}
isProjectLevel={isProjectLevel}
/>
<div
className={cn(
"border-l border-custom-border-200 transition-all",
!isProjectLevel
? "absolute right-0 top-0 bottom-0 md:relative flex-shrink-0 h-full max-w-[250px] sm:max-w-full"
: ""
)}
style={themeStore.workspaceAnalyticsSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
>
<CustomAnalyticsSidebar analytics={analytics} params={params} isProjectLevel={isProjectLevel} />
</div>
</div>
);
});

View File

@ -22,8 +22,7 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
return (
<div
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-3" : "grid-cols-2"} ${
fullScreen ? "md:py-5 lg:grid-cols-4" : ""
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-2"} ${fullScreen ? "md:py-5 lg:grid-cols-4" : ""
}`}
>
{!isProjectLevel && (

View File

@ -17,9 +17,9 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
const { getProjectById } = useProject();
return (
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
<div className="relative flex flex-col gap-4 h-full">
<h4 className="font-medium">Selected Projects</h4>
<div className="mt-4 h-full space-y-6 overflow-y-auto">
<div className="relative space-y-6 overflow-hidden overflow-y-auto">
{projectIds.map((projectId) => {
const project = getProjectById(projectId);

View File

@ -26,7 +26,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
<>
{projectId ? (
cycleDetails ? (
<div className="hidden h-full overflow-y-auto md:block">
<div className="h-full overflow-y-auto">
<h4 className="break-words font-medium">Analytics for {cycleDetails.name}</h4>
<div className="mt-4 space-y-4">
<div className="flex items-center gap-2 text-xs">
@ -52,7 +52,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
</div>
</div>
) : moduleDetails ? (
<div className="hidden h-full overflow-y-auto md:block">
<div className="h-full overflow-y-auto">
<h4 className="break-words font-medium">Analytics for {moduleDetails.name}</h4>
<div className="mt-4 space-y-4">
<div className="flex items-center gap-2 text-xs">
@ -78,7 +78,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
</div>
</div>
) : (
<div className="hidden h-full overflow-y-auto md:flex md:flex-col">
<div className="h-full overflow-y-auto">
<div className="flex items-center gap-1">
{projectDetails?.emoji ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr";
@ -19,18 +19,18 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
import { cn } from "helpers/common.helper";
type Props = {
analytics: IAnalyticsResponse | undefined;
params: IAnalyticsParams;
fullScreen: boolean;
isProjectLevel: boolean;
};
const analyticsService = new AnalyticsService();
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
const { analytics, params, fullScreen, isProjectLevel = false } = props;
const { analytics, params, isProjectLevel = false } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -138,18 +138,14 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
const selectedProjects = params.project && params.project.length > 0 ? params.project : workspaceProjectIds;
return (
<div
className={`flex items-center justify-between space-y-2 px-5 py-2.5 ${
fullScreen
? "overflow-hidden border-l border-custom-border-200 md:h-full md:flex-col md:items-start md:space-y-4 md:border-l md:border-custom-border-200 md:py-5"
: ""
}`}
<div className={cn("relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-custom-sidebar-background-100", !isProjectLevel ? "flex-col" : "")}
>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
<LayersIcon height={14} width={14} />
{analytics ? analytics.total : "..."} Issues
{analytics ? analytics.total : "..."} <div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
</div>
{isProjectLevel && (
<div className="flex items-center gap-1 rounded-md bg-custom-background-80 px-3 py-1 text-xs text-custom-text-200">
@ -164,30 +160,30 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
</div>
)}
</div>
<div className="h-full w-full overflow-hidden">
{fullScreen ? (
<div className={cn("h-full w-full overflow-hidden", isProjectLevel ? "hidden" : "block")}>
<>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
)}
<CustomAnalyticsSidebarHeader />
</>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2 justify-self-end">
<div className="flex flex-wrap items-center gap-2 justify-end">
<Button
variant="neutral-primary"
prependIcon={<RefreshCw className="h-3.5 w-3.5" />}
prependIcon={<RefreshCw className="h-3 md:h-3.5 w-3 md:w-3.5" />}
onClick={() => {
if (!workspaceSlug) return;
mutate(ANALYTICS(workspaceSlug.toString(), params));
}}
>
Refresh
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Refresh</div>
</Button>
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
Export as CSV
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Export as CSV</div>
</Button>
</div>
</div>

View File

@ -20,16 +20,15 @@ export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props
return (
<Tab.Group as={React.Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 p-5 pt-0">
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 md:px-5 py-0 md:py-3">
{ANALYTICS_TABS.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
selected ? "bg-custom-background-80" : ""
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${selected ? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200" : "border-transparent"
}`
}
onClick={() => {}}
onClick={() => { }}
>
{tab.title}
</Tab>

View File

@ -64,7 +64,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
</CustomMenu.MenuItem>
))}
</CustomMenu>
<button className="transition-all block md:hidden" onClick={() => { themStore.toggleProfileSidebar(); console.log(themStore.profileSidebarCollapsed) }}>
<button className="transition-all block md:hidden" onClick={() => { themStore.toggleProfileSidebar() }}>
<PanelRight className={
cn("w-4 h-4 block md:hidden", !themStore.profileSidebarCollapsed ? "text-[#3E63DD]" : "text-custom-text-200")
} />

View File

@ -1,13 +1,35 @@
import { useRouter } from "next/router";
import { ArrowLeft, BarChart2 } from "lucide-react";
import { BarChart2, PanelRight } from "lucide-react";
// ui
import { Breadcrumbs } from "@plane/ui";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { BreadcrumbLink } from "components/common";
import { useApplication } from "hooks/store";
import { observer } from "mobx-react";
import { cn } from "helpers/common.helper";
import { useEffect } from "react";
export const WorkspaceAnalyticsHeader = () => {
export const WorkspaceAnalyticsHeader = observer(() => {
const router = useRouter();
const { analytics_tab } = router.query;
const { theme: themeStore } = useApplication();
useEffect(() => {
const handleToggleWorkspaceAnalyticsSidebar = () => {
if (window && window.innerWidth < 768) {
themeStore.toggleWorkspaceAnalyticsSidebar(true);
}
if (window && themeStore.workspaceAnalyticsSidebarCollapsed && window.innerWidth >= 768) {
themeStore.toggleWorkspaceAnalyticsSidebar(false);
}
};
window.addEventListener("resize", handleToggleWorkspaceAnalyticsSidebar);
handleToggleWorkspaceAnalyticsSidebar();
return () => window.removeEventListener("resize", handleToggleWorkspaceAnalyticsSidebar);
}, [themeStore]);
return (
<>
@ -16,7 +38,7 @@ export const WorkspaceAnalyticsHeader = () => {
>
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<div className="flex items-center justify-between w-full">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
@ -25,9 +47,14 @@ export const WorkspaceAnalyticsHeader = () => {
}
/>
</Breadcrumbs>
{analytics_tab === 'custom' &&
<button className="block md:hidden" onClick={() => { themeStore.toggleWorkspaceAnalyticsSidebar() }}>
<PanelRight className={cn("w-4 h-4 block md:hidden", !themeStore.workspaceAnalyticsSidebarCollapsed ? "text-custom-primary-100" : "text-custom-text-200")} />
</button>
}
</div>
</div>
</div>
</>
);
};
});

View File

@ -30,7 +30,7 @@ export const ProfileSidebar = observer(() => {
const { workspaceSlug, userId } = router.query;
// store hooks
const { currentUser } = useUser();
const { theme: themStore } = useApplication();
const { theme: themeStore } = useApplication();
const ref = useRef<HTMLDivElement>(null);
const { data: userProjectsData } = useSWR(
@ -41,9 +41,9 @@ export const ProfileSidebar = observer(() => {
);
useOutsideClickDetector(ref, () => {
if (themStore.profileSidebarCollapsed === false) {
if (themeStore.profileSidebarCollapsed === false) {
if (window.innerWidth < 768) {
themStore.toggleProfileSidebar();
themeStore.toggleProfileSidebar();
}
}
});
@ -62,22 +62,22 @@ export const ProfileSidebar = observer(() => {
useEffect(() => {
const handleToggleProfileSidebar = () => {
if (window && window.innerWidth < 768) {
themStore.toggleProfileSidebar(true);
themeStore.toggleProfileSidebar(true);
}
if (window && themStore.profileSidebarCollapsed && window.innerWidth >= 768) {
themStore.toggleProfileSidebar(false);
if (window && themeStore.profileSidebarCollapsed && window.innerWidth >= 768) {
themeStore.toggleProfileSidebar(false);
}
};
window.addEventListener("resize", handleToggleProfileSidebar);
handleToggleProfileSidebar();
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
}, [themStore]);
}, [themeStore]);
return (
<div
className={`flex-shrink-0 overflow-hidden overflow-y-auto shadow-custom-shadow-sm border-l border-custom-border-100 bg-custom-sidebar-background-100 h-full z-[5] fixed md:relative transition-all w-full md:w-[300px]`}
style={themStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
style={themeStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
>
{userProjectsData ? (
<>

View File

@ -15,8 +15,11 @@ import { ANALYTICS_TABS } from "constants/analytics";
import { EUserWorkspaceRoles } from "constants/workspace";
// type
import { NextPageWithLayout } from "lib/types";
import { useRouter } from "next/router";
const AnalyticsPage: NextPageWithLayout = observer(() => {
const router = useRouter()
const { analytics_tab } = router.query
// theme
const { resolvedTheme } = useTheme();
// store hooks
@ -38,17 +41,19 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
<>
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
<div className="flex h-full flex-col overflow-hidden bg-custom-background-100">
<Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-custom-border-200 px-5 py-3">
<Tab.Group as={Fragment} defaultIndex={analytics_tab === 'custom' ? 1 : 0}>
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 md:px-5 py-0 md:py-3">
{ANALYTICS_TABS.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-3xl border border-custom-border-200 px-4 py-2 text-xs hover:bg-custom-background-80 ${
selected ? "bg-custom-background-80" : ""
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${selected ? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200" : "border-transparent"
}`
}
onClick={() => {}}
onClick={() => {
router.query.analytics_tab = tab.key
router.push(router)
}}
>
{tab.title}
</Tab>

View File

@ -8,10 +8,12 @@ export interface IThemeStore {
theme: string | null;
sidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
// actions
toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void;
toggleProfileSidebar: (collapsed?: boolean) => void;
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
}
export class ThemeStore implements IThemeStore {
@ -19,6 +21,7 @@ export class ThemeStore implements IThemeStore {
sidebarCollapsed: boolean | undefined = undefined;
theme: string | null = null;
profileSidebarCollapsed: boolean | undefined = undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
// root store
rootStore;
@ -28,10 +31,12 @@ export class ThemeStore implements IThemeStore {
sidebarCollapsed: observable.ref,
theme: observable.ref,
profileSidebarCollapsed: observable.ref,
workspaceAnalyticsSidebarCollapsed: observable.ref,
// action
toggleSidebar: action,
setTheme: action,
toggleProfileSidebar: action,
toggleWorkspaceAnalyticsSidebar: action
// computed
});
// root store
@ -64,6 +69,19 @@ export class ThemeStore implements IThemeStore {
localStorage.setItem("profile_sidebar_collapsed", this.profileSidebarCollapsed.toString());
};
/**
* Toggle the profile sidebar collapsed state
* @param collapsed
*/
toggleWorkspaceAnalyticsSidebar = (collapsed?: boolean) => {
if (collapsed === undefined) {
this.workspaceAnalyticsSidebarCollapsed = !this.workspaceAnalyticsSidebarCollapsed;
} else {
this.workspaceAnalyticsSidebarCollapsed = collapsed;
}
localStorage.setItem("workspace_analytics_sidebar_collapsed", this.workspaceAnalyticsSidebarCollapsed.toString());
};
/**
* Sets the user theme and applies it to the platform
* @param _theme