diff --git a/src/components/Aside.tsx b/src/components/Aside.tsx deleted file mode 100644 index 60aae77a6e..0000000000 --- a/src/components/Aside.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { FC, ReactNode } from "react"; -import classnames from "classnames"; -import { Spinner } from "@canonical/react-components"; - -interface Props { - className?: string; - children: ReactNode; - width?: "wide" | "narrow"; - pinned?: boolean; - loading?: boolean; - isSplit?: boolean; -} - -const Aside: FC = ({ - children, - className, - width, - pinned = false, - loading = false, - isSplit = false, -}: Props) => { - return ( -
- {loading ? ( -
- -
- ) : ( - children - )} -
- ); -}; - -export default Aside; diff --git a/src/components/DetailPanel.tsx b/src/components/DetailPanel.tsx deleted file mode 100644 index 4377831876..0000000000 --- a/src/components/DetailPanel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { FC, ReactNode } from "react"; -import { Button, Icon } from "@canonical/react-components"; -import classnames from "classnames"; -import Aside from "./Aside"; -import Loader from "./Loader"; -import usePanelParams from "util/usePanelParams"; - -interface Props { - title: string; - hasLoadingError: boolean; - className: string; - isLoading: boolean; - actions?: ReactNode; - children: ReactNode; -} - -const DetailPanel: FC = ({ - title, - hasLoadingError, - className, - isLoading, - actions, - children, -}) => { - const panelParams = usePanelParams(); - - return ( - - ); -}; - -export default DetailPanel; diff --git a/src/components/SidePanel.tsx b/src/components/SidePanel.tsx new file mode 100644 index 0000000000..60149275f5 --- /dev/null +++ b/src/components/SidePanel.tsx @@ -0,0 +1,138 @@ +import { FC, PropsWithChildren, ReactNode } from "react"; +import Loader from "components/Loader"; +import classnames from "classnames"; +import { Spinner } from "@canonical/react-components"; + +interface CommonProps { + className?: string; +} + +// Header components +const HeaderControls: FC = ({ + children, + className, +}) => { + return ( +
{children}
+ ); +}; + +const HeaderTitle: FC = ({ + children, + className, +}) => { + return ( +

{children}

+ ); +}; + +const Sticky: FC = ({ + children, + className, +}) => { + return ( +
{children}
+ ); +}; + +const Header: FC = ({ + children, + className, +}) => { + return ( +
{children}
+ ); +}; + +// Panel content components +const Container: FC = ({ + children, + className, +}) => { + return
{children}
; +}; + +const Content: FC = ({ + children, + className, +}) => { + return ( +
{children}
+ ); +}; + +// Footer components +const Footer: FC = ({ + children, + className, +}) => { + return ( +
+
+ {children} +
+ ); +}; + +interface SidePanelProps { + isOverlay?: boolean; + isSplit?: boolean; + children: ReactNode; + loading: boolean; + hasError: boolean; + className?: string; +} + +const SidePanelComponent: FC = ({ + children, + isOverlay, + isSplit = false, + loading = false, + hasError, + className, +}) => { + return ( + + ); +}; + +type SidePanelComponents = FC & { + Header: FC; + HeaderTitle: FC; + HeaderControls: FC; + Sticky: FC; + Container: FC; + Content: FC; + Footer: FC; +}; + +const SidePanel = SidePanelComponent as SidePanelComponents; +SidePanel.Header = Header; +SidePanel.HeaderTitle = HeaderTitle; +SidePanel.HeaderControls = HeaderControls; +SidePanel.Sticky = Sticky; +SidePanel.Container = Container; +SidePanel.Content = Content; +SidePanel.Footer = Footer; + +export default SidePanel; diff --git a/src/pages/instances/InstanceDetailPanel.tsx b/src/pages/instances/InstanceDetailPanel.tsx index 02f029354b..0f61794147 100644 --- a/src/pages/instances/InstanceDetailPanel.tsx +++ b/src/pages/instances/InstanceDetailPanel.tsx @@ -1,29 +1,18 @@ import { FC } from "react"; import OpenTerminalBtn from "./actions/OpenTerminalBtn"; import OpenConsoleBtn from "./actions/OpenConsoleBtn"; -import { List, useNotify } from "@canonical/react-components"; -import { isoTimeToString } from "util/helpers"; -import { isNicDevice } from "util/devices"; -import { Link } from "react-router-dom"; -import InstanceStatusIcon from "./InstanceStatusIcon"; -import { instanceCreationTypes } from "util/instanceOptions"; +import { Button, Icon, List, useNotify } from "@canonical/react-components"; import { useQuery } from "@tanstack/react-query"; import { fetchInstance } from "api/instances"; import { queryKeys } from "util/queryKeys"; import usePanelParams from "util/usePanelParams"; import InstanceStateActions from "pages/instances/actions/InstanceStateActions"; -import InstanceLink from "pages/instances/InstanceLink"; -import ItemName from "components/ItemName"; -import DetailPanel from "components/DetailPanel"; -import InstanceIps from "pages/instances/InstanceIps"; -import { useSettings } from "context/useSettings"; - -const RECENT_SNAPSHOT_LIMIT = 5; +import SidePanel from "components/SidePanel"; +import InstanceDetailPanelContent from "./InstanceDetailPanelContent"; const InstanceDetailPanel: FC = () => { const notify = useNotify(); const panelParams = usePanelParams(); - const { data: settings } = useSettings(); const { data: instance, @@ -40,230 +29,50 @@ const InstanceDetailPanel: FC = () => { notify.failure("Loading instance failed", error); } - const networkDevices = Object.values(instance?.expanded_devices ?? {}).filter( - isNicDevice, - ); - return ( - - , - , - ]} - /> -
- -
- - ) - } + - {instance && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {networkDevices.length > 0 ? ( - - - - - ) : ( - - - - )} - - - - {instance.snapshots?.length ? ( - <> - {instance.snapshots - .slice() - .sort((snap1, snap2) => { - const a = snap1.created_at; - const b = snap2.created_at; - return a > b ? -1 : a < b ? 1 : 0; - }) - .slice(0, RECENT_SNAPSHOT_LIMIT) - .map((snapshot) => ( - - - - - ))} - {instance.snapshots.length > RECENT_SNAPSHOT_LIMIT && ( - - - - )} - - ) : ( - - - - )} - -
Name - -
Base image -
- {instance.config["image.description"] ?? "-"} -
-
Status - -
Description{instance.description ? instance.description : "-"}
Type - { - instanceCreationTypes.filter( - (item) => item.value === instance.type, - )[0].label - } -
IPv4 - -
IPv6 - -
Architecture{instance.architecture}
Location - {settings?.environment?.server_clustered - ? instance.location - : "-"} -
Created{isoTimeToString(instance.created_at)}
Last used{isoTimeToString(instance.last_used_at)}
-

- - Profiles - -

-
- ( - - {name} - - ))} - /> -
-

Networks

-
- ( - - {item.network} - - ))} - /> -
-

Networks

-

- No networks found. -
- - Configure instance networks - -

-
-

- - Snapshots - -

-
- - - {isoTimeToString(snapshot.created_at)} -
- - {`View all (${instance.snapshots.length})`} - -
-

- No snapshots found. -
- - Manage instance snapshots - -

-
- )} -
+ + + + Instance summary + + + + + {instance && ( +
+ , + , + ]} + /> +
+ +
+
+ )} +
+ + + {instance && } + +
+ ); }; diff --git a/src/pages/instances/InstanceDetailPanelContent.tsx b/src/pages/instances/InstanceDetailPanelContent.tsx new file mode 100644 index 0000000000..b3eec117ad --- /dev/null +++ b/src/pages/instances/InstanceDetailPanelContent.tsx @@ -0,0 +1,221 @@ +import { FC } from "react"; +import InstanceLink from "./InstanceLink"; +import { LxdInstance } from "types/instance"; +import InstanceStatusIcon from "./InstanceStatusIcon"; +import { instanceCreationTypes } from "util/instanceOptions"; +import InstanceIps from "./InstanceIps"; +import { isoTimeToString } from "util/helpers"; +import { Link } from "react-router-dom"; +import { List } from "@canonical/react-components"; +import ItemName from "components/ItemName"; +import { useSettings } from "context/useSettings"; +import { isNicDevice } from "util/devices"; + +const RECENT_SNAPSHOT_LIMIT = 5; + +interface Props { + instance: LxdInstance; +} + +const InstanceDetailPanelContent: FC = ({ instance }) => { + const { data: settings } = useSettings(); + const networkDevices = Object.values(instance?.expanded_devices ?? {}).filter( + isNicDevice, + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {networkDevices.length > 0 ? ( + + + + + ) : ( + + + + )} + + + + {instance.snapshots?.length ? ( + <> + {instance.snapshots + .slice() + .sort((snap1, snap2) => { + const a = snap1.created_at; + const b = snap2.created_at; + return a > b ? -1 : a < b ? 1 : 0; + }) + .slice(0, RECENT_SNAPSHOT_LIMIT) + .map((snapshot) => ( + + + + + ))} + {instance.snapshots.length > RECENT_SNAPSHOT_LIMIT && ( + + + + )} + + ) : ( + + + + )} + +
Name + +
Base image +
+ {instance.config["image.description"] ?? "-"} +
+
Status + +
Description{instance.description ? instance.description : "-"}
Type + { + instanceCreationTypes.filter( + (item) => item.value === instance.type, + )[0].label + } +
IPv4 + +
IPv6 + +
Architecture{instance.architecture}
Location + {settings?.environment?.server_clustered ? instance.location : "-"} +
Created{isoTimeToString(instance.created_at)}
Last used{isoTimeToString(instance.last_used_at)}
+

+ + Profiles + +

+
+ ( + + {name} + + ))} + /> +
+

Networks

+
+ ( + + {item.network} + + ))} + /> +
+

Networks

+

+ No networks found. +
+ + Configure instance networks + +

+
+

+ + Snapshots + +

+
+ + + {isoTimeToString(snapshot.created_at)} +
+ + {`View all (${instance.snapshots.length})`} + +
+

+ No snapshots found. +
+ + Manage instance snapshots + +

+
+ ); +}; + +export default InstanceDetailPanelContent; diff --git a/src/pages/profiles/ProfileDetailPanel.tsx b/src/pages/profiles/ProfileDetailPanel.tsx index 2236811e6b..013a325e3f 100644 --- a/src/pages/profiles/ProfileDetailPanel.tsx +++ b/src/pages/profiles/ProfileDetailPanel.tsx @@ -3,15 +3,10 @@ import { useQuery } from "@tanstack/react-query"; import { queryKeys } from "util/queryKeys"; import usePanelParams from "util/usePanelParams"; import { fetchProfile } from "api/profiles"; -import ProfileLink from "./ProfileLink"; -import { isProjectWithProfiles } from "util/projects"; -import { getProfileInstances } from "util/usedBy"; -import ProfileInstances from "./ProfileInstances"; -import DetailPanel from "components/DetailPanel"; -import ProfileNetworkList from "./ProfileNetworkList"; -import ProfileStorageList from "./ProfileStorageList"; import { useProject } from "context/project"; -import { useNotify } from "@canonical/react-components"; +import { Button, Icon, useNotify } from "@canonical/react-components"; +import SidePanel from "components/SidePanel"; +import ProfileDetailPanelContent from "./ProfileDetailPanelContent"; const ProfileDetailPanel: FC = () => { const notify = useNotify(); @@ -37,80 +32,36 @@ const ProfileDetailPanel: FC = () => { const isLoading = isProfileLoading || isProjectLoading; - const featuresProfiles = isProjectWithProfiles(project); - - const isDefaultProject = projectName === "default"; - const usageCount = getProfileInstances( - projectName, - isDefaultProject, - profile?.used_by, - ).length; - return ( - - {profile && ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {usageCount > 0 ? ( - - ) : ( - - - - )} - -
Name - -
Description{profile.description ? profile.description : "-"}
Defined in{featuresProfiles ? "Current" : "Default"} project
-

Devices

-
Networks - -
Storage - -
-

- Used by ({usageCount}) -

-
No items found.
- )} -
+ + + + Profile summary + + + + + + + {!!(profile && project) && ( + + )} + + + ); }; diff --git a/src/pages/profiles/ProfileDetailPanelContent.tsx b/src/pages/profiles/ProfileDetailPanelContent.tsx new file mode 100644 index 0000000000..6ba45e4a3b --- /dev/null +++ b/src/pages/profiles/ProfileDetailPanelContent.tsx @@ -0,0 +1,85 @@ +import { FC } from "react"; +import { LxdProfile } from "types/profile"; +import ProfileLink from "./ProfileLink"; +import { getProfileInstances } from "util/usedBy"; +import ProfileNetworkList from "./ProfileNetworkList"; +import ProfileStorageList from "./ProfileStorageList"; +import ProfileInstances from "./ProfileInstances"; +import { LxdProject } from "types/project"; +import { isProjectWithProfiles } from "util/projects"; + +interface Props { + profile: LxdProfile; + project: LxdProject; +} + +const ProfileDetailPanelContent: FC = ({ profile, project }) => { + const isDefaultProject = project.name === "default"; + const usageCount = getProfileInstances( + project.name, + isDefaultProject, + profile?.used_by, + ).length; + + const featuresProfiles = isProjectWithProfiles(project); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {usageCount > 0 ? ( + + ) : ( + + + + )} + +
Name + +
Description{profile.description ? profile.description : "-"}
Defined in{featuresProfiles ? "Current" : "Default"} project
+

Devices

+
Networks + +
Storage + +
+

+ Used by ({usageCount}) +

+
No items found.
+ ); +}; + +export default ProfileDetailPanelContent; diff --git a/src/sass/_side_panel.scss b/src/sass/_side_panel.scss new file mode 100644 index 0000000000..20b50963b6 --- /dev/null +++ b/src/sass/_side_panel.scss @@ -0,0 +1,5 @@ +.is-overlay { + height: 100vh; + position: absolute; + z-index: 103 !important; +} diff --git a/src/sass/styles.scss b/src/sass/styles.scss index f43f5cb591..f3cb0a6445 100644 --- a/src/sass/styles.scss +++ b/src/sass/styles.scss @@ -88,6 +88,7 @@ $border-thin: 1px solid $color-mid-light !default; @import "scrollable_table"; @import "selectable_main_table"; @import "settings_page"; +@import "side_panel"; @import "snapshots"; @import "status_bar"; @import "storage_detail_overview";