Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {UserSettingsPage} from "@edition/user/pages/UserSettingsPage";

export default UserSettingsPage
3 changes: 3 additions & 0 deletions src/app/(dashboard)/@modal/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
12 changes: 8 additions & 4 deletions src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ interface ApplicationLayoutProps {
children: React.ReactNode
bar: React.ReactNode
tab: React.ReactNode
modal: React.ReactNode
}

const ApplicationLayout: React.FC<ApplicationLayoutProps> = ({children, bar, tab}) => {
const ApplicationLayout: React.FC<ApplicationLayoutProps> = ({children, bar, tab, modal}) => {

const client = useApolloClient()
const pathname = usePathname()
Expand Down Expand Up @@ -83,9 +84,12 @@ const ApplicationLayout: React.FC<ApplicationLayoutProps> = ({children, bar, tab
</Flex>
}>
<Layout showLayoutSplitter={false} pr={0.7} layoutGap={"0"} topContent={<>{bar}</>}>
<Layout>
<>{children}</>
</Layout>
<>
<Layout>
<>{children}</>
</Layout>
{modal}
</>
</Layout>
</Layout>
</ApplicationMiddlewareComponent>
Expand Down
3 changes: 3 additions & 0 deletions src/app/(dashboard)/users/[userId]/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {UserSettingsPage} from "@edition/user/pages/UserSettingsPage";

export default UserSettingsPage
4 changes: 3 additions & 1 deletion src/packages/ce/src/application/pages/ApplicationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ export const ApplicationPage = () => {
</Text>
</Flex>
<Spacing spacing={"xs"}/>
<Button w={"100%"} disabled color={"tertiary"}>Edit Profile</Button>
<Link href={"/users/@me/settings"}>
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
</Link>
<Spacing spacing={"xs"}/>
<Button color={"primary"} disabled w={"100%"}>
Upgrade to Pro
Expand Down
11 changes: 8 additions & 3 deletions src/packages/ce/src/application/views/ApplicationBarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
IconFolders,
IconInbox,
IconLogout,
IconSearch,
IconSearch, IconSettings,
IconUser
} from "@tabler/icons-react";
import {ApplicationBreadcrumbView} from "@edition/application/views/ApplicationBreadcrumbView";
Expand Down Expand Up @@ -151,15 +151,20 @@ export const ApplicationBarView: React.FC = () => {
}

return <UserMenuComponent userId={currentSession.user?.id!!}>
<Link href={`/users/${userIndex}`}>
<Link href={`/users/@me`}>
<MenuItem>
<IconUser size={16}/>Profile
</MenuItem>
</Link>
<Link href={`/users/@me/settings`}>
<MenuItem>
<IconSettings size={16}/>Settings
</MenuItem>
</Link>
<MenuSeparator/>
<Link href={"/workspaces"}>
<MenuItem>
<IconBuilding size={16}/>Organizations
<IconBuilding size={16}/>Workspaces
</MenuItem>
</Link>
<Link href={`/namespace/${userNamespaceIndex}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ export const NamespaceOverviewPersonalLeftView: React.FC = () => {
</Text>
</Flex>
<Spacing spacing={"xs"}/>
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
<Link href={"/users/@me/settings"}>
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
</Link>
<Spacing spacing={"xs"}/>
<Button color={"primary"} w={"100%"}>
<Button disabled color={"primary"} w={"100%"}>
Upgrade to Pro
<AuroraBackground/>
</Button>
Expand Down
50 changes: 42 additions & 8 deletions src/packages/ce/src/user/components/UserEditDialogComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ import CardSection from "@code0-tech/pictor/dist/components/card/CardSection";
import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab";
import {User, UsersUpdateInput} from "@code0-tech/sagittarius-graphql-types";
import {UserService} from "@edition/user/services/User.service";
import {useUserSession} from "@edition/user/hooks/User.session.hook";
import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook";
import {IconAt, IconBackground, IconLock, IconMail, IconSettings2, IconShieldLock} from "@tabler/icons-react";
import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout";
import {motion} from "framer-motion";
import {UserSessionsDataTableComponent} from "@edition/user/components/UserSessionsDataTableComponent";

export interface UserEditDialogComponentProps {
userId?: User['id']
Expand All @@ -49,11 +51,14 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
const userStore = useStore(UserService)
const [, startTransition] = React.useTransition()

const currentSession = useUserSession()
const user = React.useMemo(
() => userService.getById(userId),
[userStore, userId]
)

const isSelf = !!user && !!currentSession?.user?.id && user.id === currentSession.user.id

const initialValues = React.useMemo(() => ({
firstname: user?.firstname ?? null,
lastname: user?.lastname ?? null,
Expand Down Expand Up @@ -110,8 +115,10 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
if (values.firstname !== (user?.firstname ?? null)) payload.firstname = values.firstname
if (values.lastname !== (user?.lastname ?? null)) payload.lastname = values.lastname
if (values.readme !== (user?.readme ?? null)) payload.readme = values.readme
if (values.admin !== (user?.admin ?? false)) payload.admin = values.admin
if (values.blocked !== (user?.blocked ?? false)) payload.blocked = values.blocked
if (!isSelf) {
if (values.admin !== (user?.admin ?? false)) payload.admin = values.admin
if (values.blocked !== (user?.blocked ?? false)) payload.blocked = values.blocked
}
if (values.password) {
payload.password = values.password
payload.passwordRepeat = values.repeatPassword
Expand Down Expand Up @@ -185,16 +192,24 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
<Text size={"md"}>General</Text>
</Button>
</TabTrigger>
<TabTrigger value={"permissions"} w={"100%"} asChild>
{!isSelf && (
<TabTrigger value={"permissions"} w={"100%"} asChild>
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
<IconShieldLock size={13}/>
<Text size={"md"}>Access</Text>
</Button>
</TabTrigger>
)}
<TabTrigger value={"security"} w={"100%"} asChild>
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
<IconShieldLock size={13}/>
<Text size={"md"}>Access</Text>
<IconSettings2 opacity={0} size={13}/>
<Text size={"md"}>Security</Text>
</Button>
</TabTrigger>
<TabTrigger value={"security"} w={"100%"} asChild>
<TabTrigger value={"sessions"} w={"100%"} asChild>
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
<IconSettings2 opacity={0} size={13}/>
<Text size={"md"}>Security</Text>
<Text size={"md"}>Sessions</Text>
</Button>
</TabTrigger>
</TabList>
Expand Down Expand Up @@ -256,7 +271,8 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
description={"A short bio or notes shown on the user's profile."}
{...inputs.getInputProps("readme")}/>
</TabContent>
<TabContent value={"permissions"} style={{overflow: "hidden"}}>
<TabContent value={"permissions"}
style={{overflow: "hidden", display: isSelf ? "none" : undefined}}>
<Flex justify={"space-between"} align={"center"}>
<Text size={"lg"} hierarchy={"primary"} display={"block"}>Access</Text>
<Button paddingSize={"xxs"} color={"success"} variant={"none"}
Expand Down Expand Up @@ -314,6 +330,24 @@ export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (
onChange={() => validate("repeatPassword")}
{...inputs.getInputProps("repeatPassword")}/>
</TabContent>
<TabContent value={"sessions"}
style={{
overflow: "hidden",
height: "100%",
display: "flex",
flexDirection: "column"
}}>
<Flex justify={"space-between"} align={"center"}>
<Text size={"lg"} hierarchy={"primary"} display={"block"}>Sessions</Text>
</Flex>
<Spacing spacing={"xs"}/>
<Text size={"md"} hierarchy={"tertiary"}>
Active sign-in sessions for @{user?.username ?? ""}. Log out a session to revoke
its access.
</Text>
<Spacing spacing={"md"}/>
<UserSessionsDataTableComponent userId={userId}/>
</TabContent>
</Card>
</Layout>
</Tab>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client"

import React from "react";
import {
DataTable,
DataTableColumn,
ScrollArea,
ScrollAreaScrollbar, ScrollAreaThumb,
ScrollAreaViewport,
Text,
useService,
useStore
} from "@code0-tech/pictor";
import {User, UserSession} from "@code0-tech/sagittarius-graphql-types";
import {UserService} from "@edition/user/services/User.service";
import {UserSessionsDataTableRowComponent} from "@edition/user/components/UserSessionsDataTableRowComponent";

export interface UserSessionsDataTableComponentProps {
userId: User['id']
}

export const UserSessionsDataTableComponent: React.FC<UserSessionsDataTableComponentProps> = (props) => {

const {userId} = props

const userService = useService(UserService)
const userStore = useStore(UserService)

const sessions = React.useMemo(
() => (userService.getById(userId)?.sessions?.nodes ?? []).filter((session): session is UserSession => !!session && !!session.active),
[userStore, userId]
)

return <ScrollArea w={"100%"} h={"100%"} type={"scroll"}>
<ScrollAreaViewport>
<DataTable filter={{}}
sort={{}}
emptyComponent={<DataTableColumn>
<Text>No sessions found for this user.</Text>
</DataTableColumn>}
data={sessions}>
{(session) => <UserSessionsDataTableRowComponent userId={userId} sessionId={session.id}/>}
</DataTable>
</ScrollAreaViewport>
<ScrollAreaScrollbar orientation={"vertical"}>
<ScrollAreaThumb/>
</ScrollAreaScrollbar>
</ScrollArea>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client"

import React from "react";
import {User, UserSession} from "@code0-tech/sagittarius-graphql-types";
import {Button, DataTableColumn, Flex, Text, useService, useStore} from "@code0-tech/pictor";
import {UserService} from "@edition/user/services/User.service";
import {useUserSession} from "@edition/user/hooks/User.session.hook";
import {formatDistanceToNow} from "date-fns";
import {IconDeviceDesktop, IconLogout} from "@tabler/icons-react";
import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook";

export interface UserSessionsDataTableRowComponentProps {
userId: User['id']
sessionId: UserSession['id']
}

export const UserSessionsDataTableRowComponent: React.FC<UserSessionsDataTableRowComponentProps> = (props) => {

const {userId, sessionId} = props

const userService = useService(UserService)
const userStore = useStore(UserService)
const currentSession = useUserSession()

const session = React.useMemo(
() => userService.getById(userId)?.sessions?.nodes?.find(session => session?.id === sessionId) as UserSession | undefined,
[userStore, userId, sessionId]
)

const isCurrent = !!sessionId && sessionId === currentSession?.id

const logout = React.useCallback(() => {
if (!sessionId) return
userService.usersLogout({userSessionId: sessionId}).then(payload => {
if ((payload?.errors?.length ?? 0) <= 0) {
addIslandSuccessNotification({
message: "Logged out session"
})
}
})
}, [sessionId])

if (!session) return null

return <>
<DataTableColumn pr={2.5}>
<Flex align={"center"} style={{gap: "0.7rem"}}>
<IconDeviceDesktop size={16}/>
<Flex style={{flexDirection: "column"}}>
<Text size={"md"} hierarchy={"primary"}>
{isCurrent ? "This device" : "Session"}
</Text>
{session.createdAt && (
<Text size={"xs"} hierarchy={"tertiary"}>
Created {formatDistanceToNow(new Date(session.createdAt), {addSuffix: true})}
</Text>
)}
</Flex>

</Flex>
</DataTableColumn>
<DataTableColumn pr={2.5}>
{session.updatedAt && (
<Text hierarchy={"tertiary"}>
Last used {formatDistanceToNow(new Date(session.updatedAt), {addSuffix: true})}
</Text>
)}
</DataTableColumn>
<DataTableColumn>
<Button p={0.5} color={"secondary"} variant={"none"} onClick={logout}>
<IconLogout size={16}/>
</Button>
</DataTableColumn>
</>
}
13 changes: 9 additions & 4 deletions src/packages/ce/src/user/pages/UserPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from "react";
import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout";
import {useParams} from "next/navigation";
import Link from "next/link";
import {
AuroraBackground,
Avatar,
Expand All @@ -29,15 +30,17 @@ export const UserPage: React.FC = () => {
const userService = useService(UserService)
const userStore = useStore(UserService)

const userIndex = params.userId as any as number
const userId: User['id'] = `gid://sagittarius/User/${userIndex}`

const currentSession = useUserSession()
const currentUser = React.useMemo(
() => userService.getById(currentSession?.user?.id),
[userStore, currentSession]
)

const userIndex = decodeURIComponent(String(params.userId ?? ""))
const userId: User['id'] = userIndex === "@me"
? currentSession?.user?.id!
: `gid://sagittarius/User/${userIndex as any as number}`

const user = React.useMemo(
() => userService.getById(userId),
[userStore, userId, userService]
Expand Down Expand Up @@ -74,7 +77,9 @@ export const UserPage: React.FC = () => {
{currentUser?.id === userId && (
<>
<Spacing spacing={"xs"}/>
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
<Link href={"/users/@me/settings"} style={{width: "100%"}}>
<Button w={"100%"} color={"tertiary"}>Edit Profile</Button>
</Link>
<Spacing spacing={"xs"}/>
<Button color={"primary"} w={"100%"}>
Upgrade to Pro
Expand Down
Loading