diff --git a/packages/webapp/src/components/Dashboard/Dashboard.tsx b/packages/webapp/src/components/Dashboard/Dashboard.tsx index 8ab7ef365..4fb04cc23 100644 --- a/packages/webapp/src/components/Dashboard/Dashboard.tsx +++ b/packages/webapp/src/components/Dashboard/Dashboard.tsx @@ -5,6 +5,7 @@ import { Switch, Route } from 'react-router'; import '@/style/pages/Dashboard/Dashboard.scss'; import { Sidebar } from '@/containers/Dashboard/Sidebar/Sidebar'; +import { WorkspacesSidebar } from '@/containers/Dashboard/WorkspacesSidebar/WorkspacesSidebar'; import DashboardContent from '@/components/Dashboard/DashboardContent'; import DialogsContainer from '@/components/DialogsContainer'; import PreferencesPage from '@/components/Preferences/PreferencesPage'; @@ -21,10 +22,13 @@ import { DashboardSockets } from './DashboardSockets'; */ function DashboardPreferences() { return ( - - - - +
+ + + + + +
); } @@ -33,10 +37,13 @@ function DashboardPreferences() { */ function DashboardAnyPage() { return ( - - - - +
+ + + + + +
); } diff --git a/packages/webapp/src/containers/Dashboard/Sidebar/SidebarHead.tsx b/packages/webapp/src/containers/Dashboard/Sidebar/SidebarHead.tsx index 850a2ca83..7e0893321 100644 --- a/packages/webapp/src/containers/Dashboard/Sidebar/SidebarHead.tsx +++ b/packages/webapp/src/containers/Dashboard/Sidebar/SidebarHead.tsx @@ -1,19 +1,10 @@ // @ts-nocheck -import { Button, Popover, Menu, Position } from '@blueprintjs/core'; - -import { Icon } from '@/components'; - import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization'; import { useAuthenticatedAccount } from '@/hooks/query'; -import { compose, firstLettersArgs } from '@/utils'; - -// Popover modifiers. -const POPOVER_MODIFIERS = { - offset: { offset: '28, 8' }, -}; +import { compose } from '@/utils'; /** - * Sideabr head. + * Sidebar head. */ function SidebarHeadJSX({ // #withCurrentOrganization @@ -25,39 +16,12 @@ function SidebarHeadJSX({ return (
- -
- -
{organization.name}
-
- - } - position={Position.BOTTOM} - minimal={true} - > - -
+
{organization.name}
{user.full_name}
- + BC
); diff --git a/packages/webapp/src/containers/Dashboard/WorkspacesSidebar/WorkspacesSidebar.tsx b/packages/webapp/src/containers/Dashboard/WorkspacesSidebar/WorkspacesSidebar.tsx new file mode 100644 index 000000000..6a70deece --- /dev/null +++ b/packages/webapp/src/containers/Dashboard/WorkspacesSidebar/WorkspacesSidebar.tsx @@ -0,0 +1,73 @@ +// @ts-nocheck +import React from 'react'; +import { Tooltip, Position, Spinner } from '@blueprintjs/core'; +import { useWorkspaces } from '@/hooks/query'; +import { useAuthOrganizationId } from '@/hooks/state'; +import { useSwitchOrganization } from '@/hooks/useSwitchOrganization'; +import { firstLettersArgs } from '@/utils'; +import classNames from 'classnames'; + +import '@/style/containers/Dashboard/WorkspacesSidebar.scss'; + +/** + * Single workspace icon button. + */ +function WorkspaceIcon({ workspace, isActive, onClick }) { + const name = workspace.metadata?.name || workspace.organizationId; + const initials = firstLettersArgs(...(name || '').split(' ')); + const isDisabled = !workspace.isReady || workspace.isBuildRunning; + + return ( + + + + ); +} + +/** + * Workspaces sidebar container. + */ +export function WorkspacesSidebar() { + const { data: workspaces, isLoading } = useWorkspaces(); + const activeOrganizationId = useAuthOrganizationId(); + const switchOrganization = useSwitchOrganization(); + + return ( +
+
+ {isLoading ? ( +
+ +
+ ) : ( + workspaces?.map((workspace) => ( + + )) + )} +
+
+ ); +} diff --git a/packages/webapp/src/hooks/query/index.tsx b/packages/webapp/src/hooks/query/index.tsx index 82fb35b5a..f69981ea5 100644 --- a/packages/webapp/src/hooks/query/index.tsx +++ b/packages/webapp/src/hooks/query/index.tsx @@ -23,6 +23,7 @@ export * from './exchangeRates'; export * from './contacts'; export * from './subscriptions'; export * from './organization'; +export * from './workspaces'; export * from './landedCost'; export * from './UniversalSearch/UniversalSearch'; export * from './GenericResource'; diff --git a/packages/webapp/src/hooks/query/workspaces.tsx b/packages/webapp/src/hooks/query/workspaces.tsx new file mode 100644 index 000000000..b9b0484aa --- /dev/null +++ b/packages/webapp/src/hooks/query/workspaces.tsx @@ -0,0 +1,22 @@ +// @ts-nocheck +import { useRequestQuery } from '../useQueryRequest'; + +/** + * Retrieve workspaces of the authenticated user. + */ +export function useWorkspaces(props) { + return useRequestQuery( + ['workspaces'], + { method: 'get', url: 'workspaces' }, + { + select: (res) => res.data.workspaces, + initialDataUpdatedAt: 0, + initialData: { + data: { + workspaces: [], + }, + }, + ...props, + }, + ); +} diff --git a/packages/webapp/src/hooks/useSwitchOrganization.tsx b/packages/webapp/src/hooks/useSwitchOrganization.tsx new file mode 100644 index 000000000..8d8e1e500 --- /dev/null +++ b/packages/webapp/src/hooks/useSwitchOrganization.tsx @@ -0,0 +1,25 @@ +// @ts-nocheck +import { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { useQueryClient } from 'react-query'; +import { setCookie } from '@/utils'; +import { setOrganizationId } from '@/store/authentication/authentication.actions'; + +/** + * Switches the active organization by updating the cookie, + * Redux state, clearing the query cache, and reloading the app. + */ +export function useSwitchOrganization() { + const dispatch = useDispatch(); + const queryClient = useQueryClient(); + + return useCallback( + (organizationId: string) => { + setCookie('organization_id', organizationId); + dispatch(setOrganizationId(organizationId)); + queryClient.removeQueries(); + window.location.assign('/'); + }, + [dispatch, queryClient], + ); +} diff --git a/packages/webapp/src/style/containers/Dashboard/WorkspacesSidebar.scss b/packages/webapp/src/style/containers/Dashboard/WorkspacesSidebar.scss new file mode 100644 index 000000000..76c239724 --- /dev/null +++ b/packages/webapp/src/style/containers/Dashboard/WorkspacesSidebar.scss @@ -0,0 +1,96 @@ +@import 'src/style/_base.scss'; + +.workspaces-sidebar { + width: 64px; + height: 100vh; + background: $sidebar-background; + display: flex; + flex-direction: column; + align-items: center; + padding: 16px 0; + border-right: 1px solid rgba(255, 255, 255, 0.05); + z-index: $sidebar-zindex + 1; + + &__list { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + width: 100%; + } + + &__item { + width: 40px; + height: 40px; + border-radius: 8px; + border: 0; + background: rgba(255, 255, 255, 0.1); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s ease, transform 0.1s ease, border-radius 0.15s ease; + position: relative; + + &:hover { + background: rgba(255, 255, 255, 0.2); + transform: scale(1.05); + } + + &.is-active { + background: rgba(255, 255, 255, 0.9); + color: $sidebar-background; + border-radius: 12px; + + &::before { + content: ''; + position: absolute; + left: -8px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 24px; + background: #fff; + border-radius: 0 2px 2px 0; + } + } + + &.is-disabled { + opacity: 0.5; + cursor: not-allowed; + + &:hover { + background: rgba(255, 255, 255, 0.1); + transform: none; + } + } + + .bp4-spinner { + .bp4-spinner-head { + stroke: rgba(255, 255, 255, 0.8); + } + } + + &.is-active .bp4-spinner .bp4-spinner-head { + stroke: $sidebar-background; + } + } + + &__item-initials { + font-size: 14px; + font-weight: 600; + line-height: 1; + } + + &__loading { + display: flex; + align-items: center; + justify-content: center; + padding-top: 20px; + + .bp4-spinner .bp4-spinner-head { + stroke: rgba(255, 255, 255, 0.8); + } + } +} diff --git a/packages/webapp/src/style/pages/Dashboard/Dashboard.scss b/packages/webapp/src/style/pages/Dashboard/Dashboard.scss index 9b960ed84..642f3f21a 100644 --- a/packages/webapp/src/style/pages/Dashboard/Dashboard.scss +++ b/packages/webapp/src/style/pages/Dashboard/Dashboard.scss @@ -1,6 +1,15 @@ @import '../../_base.scss'; $dashboard-views-bar-height: 44px; +.dashboard-layout { + display: flex; + height: 100vh; + + > .split-pane { + flex: 1 1 auto; + } +} + .dashboard { display: flex; height: 100vh;