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 (
-
-
-
- {firstLettersArgs(...(organization.name || '').split(' '))}{' '}
-
-
{organization.name}
-
-
- }
- position={Position.BOTTOM}
- minimal={true}
- >
- }
- >
- {organization.name}
-
-
+
{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;