GitFOSS
feat(settings): use new drawer layout
+ 442
- 125
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1778587082588,
+  "_generatedAtUnix": 1778595194575,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "assets": {

...
@@ -102,7 +102,7 @@
   },
   "views": {
     "HomeView": {
-      "hash": "e6217782009d142a7170240f5aff23b499d9cec2",
+      "hash": "02d7348a7966d0bab30badacd77703cec1446515",
       "pathSource": "./app/views/HomeView.tsx"
     },
     "InternalErrorView": {

...
@@ -166,27 +166,27 @@
       "pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestsView.tsx"
     },
     "SettingsKeyAddView": {
-      "hash": "49a739a0d6783448980745c0ac979f9811b4bb47",
+      "hash": "0fc5e575a25831c60103cdf614fd0da91c644bef",
       "pathSource": "./app/views/settings/SettingsKeyAddView.tsx"
     },
     "SettingsKeyUpdateView": {
-      "hash": "bb7caeb011500b55d56f851d8182678f3ed9ff08",
+      "hash": "f0800fd51d6b19ee0e9e91ee8f8a8bb58d42a7a4",
       "pathSource": "./app/views/settings/SettingsKeyUpdateView.tsx"
     },
     "SettingsKeysListView": {
-      "hash": "512650c4e1b5ea9e2127ec4f2ef68d40ab70c6fb",
+      "hash": "77faa783fcef9d4b16d5c6b26674de99c15041cf",
       "pathSource": "./app/views/settings/SettingsKeysListView.tsx"
     },
     "SettingsView": {
-      "hash": "ca5ade3fa3a5c34faaa321fbdb5bc844f46ce37a",
+      "hash": "d57608080980620f2458c4931e14983ace10b908",
       "pathSource": "./app/views/settings/SettingsView.tsx"
     },
     "UserDashboardView": {
-      "hash": "2f9e8e3393a4c63f5ba6c0d088d5d23f83a6fac3",
+      "hash": "192015222f52bbf1cdf5f0f32a313743748bfe59",
       "pathSource": "./app/views/user/UserDashboardView.tsx"
     },
     "UserDetailsView": {
-      "hash": "05ab81a08fffc02b366ae34460d482c863750b33",
+      "hash": "d2a656ba2a48011e0a2111a6eff6a1484e0ebfed",
       "pathSource": "./app/views/user/UserDetailsView.tsx"
     }
   }

app/components/DrawerPrimary.tsx
@@ -80,7 +80,7 @@ export const DrawerPrimary = ({
       <StyledDrawerContent>
         <StyledDrawerListHeader>
           <a href={buildRouteLink(AppRoute.ORGANIZATION_DETAILS, { orgSlug })}>
-            {orgSlug}
+            @{orgSlug}
           </a>
           <span>{" / "}</span>
           <a

new file
app/components/DrawerSettings.tsx
@@ -0,0 +1,297 @@
+// 3rd-party
+import React from "react";
+import styled, { css } from "styled-components";
+// app
+import { Const } from "../const";
+import { Chip } from "./Chip";
+import { NamedColors } from "../utils/style";
+import {
+  type RepositoryCountersDTO,
+  type CommonViewProps,
+  type WithThemeSchemeProp,
+} from "../types";
+import { buildRouteLink } from "../utils/shared";
+import { AppRoute } from "../routes.defs";
+
+export const DrawerSettings = ({
+  visible = false,
+  commonProps,
+  themeScheme,
+  username,
+  counters = {
+    sshKeys: 0,
+  },
+}: WithThemeSchemeProp & {
+  visible: boolean;
+  commonProps: CommonViewProps;
+  username: string;
+  counters?: RepositoryCountersDTO;
+}) => {
+  const pathMyAccount = buildRouteLink(
+    AppRoute.USER_DETAILS,
+    {
+      username: username,
+    },
+    { encodeURIComponent: false },
+  );
+
+  const pathSSHKeys = buildRouteLink(
+    AppRoute.SETTINGS_KEYS,
+    {
+      username: username,
+    },
+    { encodeURIComponent: false },
+  );
+
+  if (visible === false) {
+    return null;
+  }
+
+  console.log("counters:", counters);
+
+  return (
+    <StyledDrawerSettings themeScheme={themeScheme}>
+      <StyledDrawerHeader>
+        <StyledLogoArea themeScheme={themeScheme}>
+          <a href={"/"}>
+            <h1>{Const.APP_NAME}</h1>
+          </a>
+        </StyledLogoArea>
+      </StyledDrawerHeader>
+      <StyledDrawerContent>
+        <StyledDrawerListHeader>
+          <div
+            style={{
+              width: 20,
+              height: 20,
+              background: "red",
+              borderRadius: 16,
+              marginRight: 8,
+            }}
+          />
+          <a href={buildRouteLink(AppRoute.USER_DETAILS, { username })}>
+            @{username}
+          </a>
+        </StyledDrawerListHeader>
+        <StyledDrawerList>
+          <StyledDrawerListItem
+            themeScheme={themeScheme}
+            href={pathMyAccount}
+            className={
+              // commonProps.path!.startsWith(pathMyAccount) ||
+              commonProps.path! === pathMyAccount ? "active" : undefined
+            }
+          >
+            <span>My Account</span>
+          </StyledDrawerListItem>
+          <StyledDrawerListItem
+            themeScheme={themeScheme}
+            href={pathSSHKeys}
+            className={
+              commonProps.path! === pathSSHKeys ||
+              commonProps.path!.startsWith(pathSSHKeys)
+                ? "active"
+                : undefined
+            }
+          >
+            <span>SSH Keys</span>
+            <Chip>{counters.sshKeys || 0}</Chip>
+          </StyledDrawerListItem>
+        </StyledDrawerList>
+        <StyledDrawerListHeader></StyledDrawerListHeader>
+        <StyledDrawerList></StyledDrawerList>
+      </StyledDrawerContent>
+      <StyledDrawerFooter>
+        <StyledDrawerList>
+          <StyledDrawerListItem themeScheme={themeScheme} disabled>
+            <span>Feedback</span>
+          </StyledDrawerListItem>
+          {/*<StyledDrawerListItem themeScheme={themeScheme} disabled>
+            <span>Help Center</span>
+            {counters.helpCenterNotifs! > 0 && (
+              <Chip>{counters.helpCenterNotifs}</Chip>
+            )}
+          </StyledDrawerListItem>*/}
+          <StyledDrawerListItem
+            themeScheme={themeScheme}
+            href={buildRouteLink(AppRoute.AUTH_LOGOUT_ACTION, null)}
+          >
+            <span>Logout</span>
+          </StyledDrawerListItem>
+        </StyledDrawerList>
+      </StyledDrawerFooter>
+    </StyledDrawerSettings>
+  );
+};
+
+const StyledDrawerSettings = styled.aside<
+  WithThemeSchemeProp & { color?: string }
+>`
+  ${({ themeScheme }) => css`
+    min-width: 300px;
+    max-width: 320px;
+    height: 100vh;
+
+    display: flex;
+    flex-flow: column nowrap;
+    align-items: center;
+    justify-content: center;
+
+    position: sticky;
+    top: 0;
+    left: 0;
+    bottom: 0;
+
+    background: ${NamedColors.HEADER[themeScheme]};
+    border-right: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]};
+
+    @media only screen and (max-width: 768px) {
+      display: none;
+    }
+  `};
+`;
+
+const StyledDrawerHeader = styled.header`
+  width: 100%;
+  height: 64px;
+
+  display: flex;
+  justify-content: center;
+  align-items: center;
+`;
+
+const StyledLogoArea = styled.div<WithThemeSchemeProp>`
+  @media only screen and (max-width: 768px) {
+    & > a > h1 {
+      font-size: 22px;
+    }
+  }
+
+  & > a {
+    display: flex;
+    flex-flow: row nowrap;
+    justify-content: flex-start;
+    align-items: center;
+
+    ${({ themeScheme }) => css`
+      color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
+    `};
+
+    h1 {
+      margin: 0;
+    }
+  }
+`;
+
+const StyledDrawerContent = styled.main`
+  width: 100%;
+  max-width: 100%;
+  min-width: 100%;
+  height: 100%;
+
+  flex: 1;
+
+  padding: 12px;
+  overflow-y: auto;
+`;
+
+const StyledDrawerListHeader = styled.section`
+  width: 100%;
+  min-height: 40px;
+
+  display: flex;
+  flex-flow: row wrap;
+  justify-content: center;
+  align-items: center;
+
+  font-weight: bold;
+
+  margin-bottom: 12px;
+
+  & > span {
+    margin: 0 4px;
+  }
+`;
+
+const StyledDrawerList = styled.section`
+  width: 100%;
+
+  display: flex;
+  flex-flow: column nowrap;
+  justify-content: flex-start;
+  align-items: center;
+
+  gap: 2px;
+`;
+
+const StyledDrawerListItem = styled.a<
+  WithThemeSchemeProp & { disabled?: boolean }
+>`
+  ${({ disabled, themeScheme }) => css`
+    width: 100%;
+    height: 40px;
+
+    display: flex;
+    flex-flow: row nowrap;
+    justify-content: flex-start;
+    align-items: center;
+
+    padding: 0 12px;
+
+    font-weight: normal;
+    font-size: 14px;
+    color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
+    border-radius: 20px;
+    text-decoration: none;
+
+    & > span:nth-child(1) {
+      flex: 1;
+    }
+
+    & > ${Chip} {
+      color: ${NamedColors.TEXT_MUTED[themeScheme]};
+      background-color: ${NamedColors.CARD_OVERLAY[themeScheme]};
+    }
+
+    ${(disabled == null || disabled === false) &&
+    css`
+      &.active,
+      &:not(:disabled):hover {
+        color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
+        background-color: ${NamedColors.CARD[themeScheme]};
+        font-weight: bold;
+        font-family: monospace;
+      }
+    `};
+
+    &:hover {
+      text-decoration: none;
+    }
+
+    &:disabled {
+      color: ${NamedColors.TEXT_MUTED[themeScheme]};
+    }
+
+    ${disabled &&
+    css`
+      color: ${NamedColors.TEXT_MUTED[themeScheme]};
+
+      & > ${Chip} {
+        color: ${NamedColors.TEXT_MUTED[themeScheme]};
+        opacity: 0.3;
+      }
+    `}
+  `}
+`;
+
+const StyledDrawerFooter = styled.footer`
+  width: 100%;
+  height: 128px;
+
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+
+  margin: 16px 0;
+  padding: 0 12px;
+`;

app/components/Layout.tsx
@@ -11,6 +11,7 @@ import InstantRouterIndicator from "../islands/InstantRouterIndicator";
 // app components
 import { PageHeader } from "./PageHeader";
 import { DrawerPrimary } from "./DrawerPrimary";
+import { DrawerSettings } from "./DrawerSettings";
 
 const BRANDLINE_HEIGHT = 4;
 const HEADER_HEIGHT = 64;

...
@@ -23,6 +24,8 @@ const LayoutComponent: FC<LayoutProps & WithThemeSchemeProp> = (props) => {
     children,
     gitStamp,
     themeScheme,
+    showDrawerSettings = false,
+    username,
     showDrawerPrimary = false,
     orgSlug = "",
     repoSlug = "",

...
@@ -37,6 +40,8 @@ const LayoutComponent: FC<LayoutProps & WithThemeSchemeProp> = (props) => {
 
   const [drawerPrimaryOpen, setDrawerPrimaryOpen] =
     useState<boolean>(showDrawerPrimary);
+  const [drawerSettingsOpen, setDrawerSettingsOpen] =
+    useState<boolean>(showDrawerSettings);
 
   return (
     <>

...
@@ -98,6 +103,13 @@ const LayoutComponent: FC<LayoutProps & WithThemeSchemeProp> = (props) => {
             currentRef={currentRef}
             path={path}
           />
+          <DrawerSettings
+            commonProps={props as any}
+            themeScheme={themeScheme}
+            visible={drawerSettingsOpen}
+            counters={layoutCounters}
+            username={username!}
+          />
           <StyledChildrenWrapper
             {...sharedProps}
             showDrawerPrimary={drawerPrimaryOpen}

...
@@ -106,8 +118,11 @@ const LayoutComponent: FC<LayoutProps & WithThemeSchemeProp> = (props) => {
               <PageHeader
                 commonProps={props as any}
                 themeScheme={themeScheme}
-                forceShowLogo={showDrawerPrimary !== true}
+                forceShowLogo={
+                  showDrawerPrimary !== true && showDrawerSettings !== true
+                }
                 setDrawerPrimaryOpen={setDrawerPrimaryOpen}
+                setDrawerSettingsOpen={setDrawerSettingsOpen}
               />
             </StyledPageHeaderWrapper>
             {children}

app/components/PageHeader.tsx
@@ -12,6 +12,7 @@ import { PageWrapper } from "./PageWrapper";
 interface PageHeaderProps extends CommonProps {
   forceShowLogo?: boolean;
   setDrawerPrimaryOpen?: (predicate: (prev: boolean) => boolean) => void;
+  setDrawerSettingsOpen?: (predicate: (prev: boolean) => boolean) => void;
 }
 
 export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({

...
@@ -19,6 +20,7 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
   themeScheme,
   forceShowLogo = true,
   setDrawerPrimaryOpen = undefined,
+  setDrawerSettingsOpen = undefined,
 }) => {
   const invertThemeScheme = themeScheme === "light" ? "dark" : "light";
 

...
@@ -26,6 +28,9 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
     if (setDrawerPrimaryOpen) {
       setDrawerPrimaryOpen((prev) => !prev);
     }
+    if (setDrawerSettingsOpen) {
+      setDrawerSettingsOpen((prev) => !prev);
+    }
   };
 
   const pageHeaderActions = useMemo(() => {

app/components/index.ts
@@ -10,3 +10,5 @@ export { PageHeader } from "./PageHeader";
 export { PageWrapper } from "./PageWrapper";
 export { TextEllipsis } from "./TextEllipsis.styled";
 export { TextInput, TextArea, Select } from "./TextInput.styled";
+export { DrawerPrimary } from "./DrawerPrimary";
+export { DrawerSettings } from "./DrawerSettings";

app/controllers/settings/keys/getKeysListView.ts
@@ -1,16 +1,35 @@
 // 1st-party
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRouteParams } from "app/routes.defs";
+import { AppRoute, AppRouteParams } from "../../../routes.defs";
+import { makeUsersService } from "../../../services/user";
 // app views
-import SettingsKeysListView from "../../../views/settings/SettingsKeysListView";
+// import SettingsKeysListView from "../../../views/settings/SettingsKeysListView";
+import SettingsView, {
+  SettingsViewProps,
+} from "../../../views/settings/SettingsView";
 
 const getKeysListView: ReqHandler<
   AppRouteParams,
   AppRoute.SETTINGS_KEYS
 > = async (request, reply) => {
+  const { curr_user_username } = request.session.data;
+
+  if (curr_user_username == null) {
+    return reply.status(404).callNotFound();
+  }
+
+  const usersService = makeUsersService({ request });
+  const user = await usersService.getUserByUsername(curr_user_username);
+
+  if (user == null) {
+    return reply.status(404).callNotFound();
+  }
+
   const reqHandler = reply.makeRequestHandler(request, reply);
-  return reqHandler(SettingsKeysListView.name, {});
+  return reqHandler<SettingsViewProps>(SettingsView.name, {
+    sshKeys: await usersService.getUserSSHKeys(user),
+  });
 };
 
 export default getKeysListView;

app/services/repository/getRepositoryCounters.ts
@@ -11,10 +11,10 @@ const CACHE_TTL = 30 * 1000; // 30s before cache expires
 
 const makeGetRepositoryCounters: ServiceMethodFactory<
   RepositoryServiceDeps,
-  [string, string],
+  [string, string, string | null],
   Promise<RepositoryCountersDTO | null>
 > = ({ request }) => {
-  return async (orgSlug, repoSlug) => {
+  return async (orgSlug, repoSlug, username) => {
     const cacheKey = `${orgSlug}/${repoSlug}/counters`;
     const lastRefresh = await CACHE.get(
       `${orgSlug}/${repoSlug}/counters/lastRefresh`,

...
@@ -49,6 +49,19 @@ const makeGetRepositoryCounters: ServiceMethodFactory<
       },
     });
 
+    // retrieve current user ssh keyrs count
+    const sshKeys =
+      username != null
+        ? await request.prisma.userSSHKey.count({
+            where: {
+              user: {
+                username: username!,
+              },
+              revoked: false,
+            },
+          })
+        : 0;
+
     // for now provide pulls and zeros
     const counters: RepositoryCountersDTO = {
       files: 0,

...
@@ -62,6 +75,7 @@ const makeGetRepositoryCounters: ServiceMethodFactory<
       issues: 0,
       apiRefSymbols: 0,
       helpCenterNotifs: 0,
+      sshKeys: sshKeys,
     };
 
     await CACHE.set(cacheKey, JSON.stringify(counters));

...
@@ -81,6 +95,7 @@ const DEFAULT_COUNTERS: RepositoryCountersDTO = {
   issues: 0,
   apiRefSymbols: 0,
   helpCenterNotifs: 0,
+  sshKeys: 0,
 };
 
 export default makeGetRepositoryCounters;

app/services/repository/types.ts
@@ -124,6 +124,7 @@ export interface RepositoryServiceAPI extends ServiceApiContract {
   getRepositoryCounters(
     orgSlug: string,
     repoSlug: string,
+    username: string | null,
   ): Promise<RepositoryCountersDTO>;
 }
 

@@ -46,6 +46,8 @@ export type CommonProps = { commonProps: CommonViewProps };
 export interface LayoutProps extends CommonViewProps {
   foo?: boolean;
   appVersion: string;
+  showDrawerSettings?: boolean;
+  username?: string;
   showDrawerPrimary?: boolean;
   orgSlug?: string;
   repoSlug?: string;

...
@@ -217,4 +219,5 @@ export interface RepositoryCountersDTO {
   issues?: number;
   apiRefSymbols?: number;
   helpCenterNotifs?: number;
+  sshKeys?: number;
 }

app/utils/server/loadRepositoryCounters.ts
@@ -20,7 +20,7 @@ export const loadRepositoryCounters: preHandlerHookHandler = async (
   reply,
   done,
 ) => {
-  const { orgSlug, repoSlug } = request.params as any;
+  const { orgSlug, repoSlug, username } = request.params as any;
   if (orgSlug == null || repoSlug == null) {
     reply.context.layoutCounters = {
       files: 0,

...
@@ -34,12 +34,17 @@ export const loadRepositoryCounters: preHandlerHookHandler = async (
       issues: 0,
       apiRefSymbols: 0,
       helpCenterNotifs: 0,
+      sshKeys: 0,
     };
     done();
     return;
   }
   const repoService = makeRepositoryService({ request });
-  const counters = await repoService.getRepositoryCounters(orgSlug, repoSlug);
+  const counters = await repoService.getRepositoryCounters(
+    orgSlug,
+    repoSlug,
+    username,
+  );
   reply.context.layoutCounters = counters;
   done();
 };

app/views/settings/SettingsKeyAddView.tsx
@@ -31,7 +31,11 @@ const SettingsKeyAddView: ReactView<SettingsKeyAddViewProps> = ({
 }) => {
   const username = commonProps.currentUserUsername!;
   return (
-    <Layout {...commonProps}>
+    <Layout
+      {...commonProps}
+      showDrawerSettings
+      username={commonProps.currentUserUsername!}
+    >
       <PageWrapper>
         <Grid.Col fluid nowrap gap={8}>
           <h1 style={{ margin: 0 }}>Add Key</h1>

...
@@ -41,7 +45,7 @@ const SettingsKeyAddView: ReactView<SettingsKeyAddViewProps> = ({
               action={buildRouteLink(
                 AppRoute.SETTINGS_KEY_ADD_ACTION,
                 { username: username },
-                { encodeURIComponent: 0 }
+                { encodeURIComponent: 0 },
               )}
             >
               <Grid.Col fluid nowrap gap={16} alignItems={"flex-end"}>

app/views/settings/SettingsKeyUpdateView.tsx
@@ -12,7 +12,11 @@ const SettingsKeyUpdateView: ReactView<SettingsKeyUpdateViewProps> = ({
   commonProps,
 }) => {
   return (
-    <Layout {...commonProps}>
+    <Layout
+      {...commonProps}
+      showDrawerSettings
+      username={commonProps.currentUserUsername!}
+    >
       <PageWrapper>
         <h1>Key Update View</h1>
       </PageWrapper>

app/views/settings/SettingsKeysListView.tsx
@@ -12,7 +12,11 @@ const SettingsKeysListView: ReactView<SettingsKeysListViewProps> = ({
   commonProps,
 }) => {
   return (
-    <Layout {...commonProps}>
+    <Layout
+      {...commonProps}
+      showDrawerSettings
+      username={commonProps.currentUserUsername!}
+    >
       <PageWrapper>
         <h1>Keys List View</h1>
       </PageWrapper>

app/views/settings/SettingsView.tsx
@@ -8,10 +8,8 @@ import { UserSSHKey } from "@prisma/client";
 import type { CommonProps } from "../../types";
 import { buildRouteLink } from "../../utils/shared";
 import { AppRoute } from "../../routes.defs";
-import { Chip } from "../../components/Chip";
 import {
   ButtonAnchor,
-  Card,
   Grid,
   IslandWrapper,
   Layout,

...
@@ -29,84 +27,44 @@ const SettingsView: ReactView<SettingsViewProps> = ({
   sshKeys,
 }) => {
   return (
-    <Layout {...commonProps}>
+    <Layout
+      {...commonProps}
+      showDrawerSettings
+      username={commonProps.currentUserUsername!}
+    >
       <PageWrapper>
-        <Grid.Col fluid nowrap gap={8}>
-          <h1 style={{ margin: 0 }}>Settings</h1>
-          <Grid.Row fluid gap={16}>
-            <Grid.Col flex={0.33} nowrap>
-              <Card themeScheme={commonProps.themeScheme}>
-                <Grid.Col fluid nowrap gap={8}>
-                  <a
-                    style={{ width: "100%" }}
-                    aria-label={"Manage your profile"}
-                    href={buildRouteLink(
-                      AppRoute.USER_DETAILS,
-                      {
-                        username: commonProps.currentUserUsername!,
-                      },
-                      { encodeURIComponent: false },
-                    )}
-                  >
-                    <Grid.Row fluid nowrap gap={8} alignItems={"center"}>
-                      <span style={{ flex: 1 }}>My Profile</span>
-                    </Grid.Row>
-                  </a>
-                  <a
-                    style={{ width: "100%" }}
-                    aria-label={"Manage your SSH keys"}
-                    href={buildRouteLink(
-                      AppRoute.SETTINGS_KEYS,
-                      {
-                        username: commonProps.currentUserUsername!,
-                      },
-                      { encodeURIComponent: false },
-                    )}
-                  >
-                    <Grid.Row fluid nowrap gap={8} alignItems={"center"}>
-                      <span style={{ flex: 1 }}>SSH Keys</span>
-                      <Chip color={"white"}>{sshKeys.length}</Chip>
-                    </Grid.Row>
-                  </a>
-                </Grid.Col>
-              </Card>
-            </Grid.Col>
-            <Grid.Col fluid nowrap>
-              <Card themeScheme={commonProps.themeScheme}>
-                <Grid.Col fluid nowrap gap={8}>
-                  <Grid.Row
-                    fluid
-                    nowrap
-                    justifyContent="space-between"
-                    gap={8}
-                    alignItems="center"
-                  >
-                    <h2 style={{ margin: 0 }}>Your SSH keys</h2>
-                    <ButtonAnchor
-                      href={buildRouteLink(
-                        AppRoute.SETTINGS_KEY_ADD,
-                        { username: commonProps.currentUserUsername! },
-                        { encodeURIComponent: 0 },
-                      )}
-                    >
-                      Add
-                    </ButtonAnchor>
-                  </Grid.Row>
-                  {sshKeys.map((key, idx) => (
-                    <IslandWrapper
-                      data-islandid={`${SSHKeyItem.name}$$${idx}`}
-                      key={key.id}
-                    >
-                      <SSHKeyItem
-                        themeScheme={commonProps.themeScheme}
-                        sshKey={key}
-                      />
-                    </IslandWrapper>
-                  ))}
-                </Grid.Col>
-              </Card>
-            </Grid.Col>
+        <Grid.Col fluid nowrap gap={24}>
+          <Grid.Row
+            fluid
+            nowrap
+            justifyContent="space-between"
+            gap={8}
+            alignItems="center"
+          >
+            <h1 style={{ margin: 0 }}>SSH Keys</h1>
+            <ButtonAnchor
+              href={buildRouteLink(
+                AppRoute.SETTINGS_KEY_ADD,
+                { username: commonProps.currentUserUsername! },
+                { encodeURIComponent: 0 },
+              )}
+            >
+              Add
+            </ButtonAnchor>
           </Grid.Row>
+          <Grid.Col fluid nowrap gap={12}>
+            {sshKeys.map((key, idx) => (
+              <IslandWrapper
+                key={key.id}
+                data-islandid={`${SSHKeyItem.name}$$${idx}`}
+              >
+                <SSHKeyItem
+                  themeScheme={commonProps.themeScheme}
+                  sshKey={key}
+                />
+              </IslandWrapper>
+            ))}
+          </Grid.Col>
         </Grid.Col>
       </PageWrapper>
     </Layout>

app/views/user/UserDashboardView.tsx
@@ -21,7 +21,11 @@ const UserDashboardView: ReactView<UserDashboardViewProps> = ({
   repositories,
 }) => {
   return (
-    <Layout {...commonProps}>
+    <Layout
+      {...commonProps}
+      // showDrawerSettings
+      // username={commonProps.currentUserUsername!}
+    >
       <PageWrapper>
         {/* <h1>Hey {currentUser.displayName || currentUser.username}, welcome!</h1> */}
         <h1 style={{ opacity: 0.67 }}>Repositories</h1>

app/views/user/UserDetailsView.tsx
@@ -6,15 +6,7 @@ import React from "react";
 import type { Organization, Repository, User } from "@prisma/client";
 // app
 import type { CommonProps } from "../../types";
-import { buildRouteLink } from "../../utils/shared";
-import { AppRoute } from "../../routes.defs";
-import {
-  ButtonAnchor,
-  Grid,
-  IslandWrapper,
-  Layout,
-  PageWrapper,
-} from "../../components";
+import { Grid, IslandWrapper, Layout, PageWrapper } from "../../components";
 // app islands
 import RepositoriesList from "../../islands/RepositoriesList";
 

...
@@ -31,7 +23,11 @@ const UserDetailsView: ReactView<UserDetailsViewProps> = ({
   repositories,
 }) => {
   return (
-    <Layout {...commonProps}>
+    <Layout
+      {...commonProps}
+      showDrawerSettings={currentUser != null && currentUser!.id === user.id}
+      username={user.username!}
+    >
       <PageWrapper>
         <Grid.Row fluid gap={8}>
           <Grid.Col fluid nowrap gap={4}>

...
@@ -44,21 +40,6 @@ const UserDetailsView: ReactView<UserDetailsViewProps> = ({
                   }`}
             </h2>
           </Grid.Col>
-          <ButtonAnchor
-            href={buildRouteLink(
-              AppRoute.SETTINGS,
-              { username: user.username },
-              { encodeURIComponent: 0 }
-            )}
-          >
-            Settings
-          </ButtonAnchor>
-          <ButtonAnchor
-            href={buildRouteLink(AppRoute.AUTH_LOGOUT_ACTION, {})}
-            style={{ backgroundColor: "#f44d4d" }}
-          >
-            Logout
-          </ButtonAnchor>
         </Grid.Row>
         <IslandWrapper data-islandid={`${RepositoriesList.name}$$0`}>
           <RepositoriesList