forked from github/plane
style: responsive analytics (#3604)
This commit is contained in:
parent
2e129682b7
commit
3a14f19c99
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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 && (
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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")
|
||||
} />
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user