1
Fork 0
mirror of https://github.com/Steffo99/sophon.git synced 2024-12-23 07:14:21 +00:00

🚧 idk notebook stuff

This commit is contained in:
Steffo 2021-10-14 05:05:01 +02:00 committed by Stefano Pigozzi
parent 458ce19a62
commit 543946fc56
14 changed files with 472 additions and 18 deletions

View file

@ -39,6 +39,35 @@ class NotebooksViewSet(SophonGroupViewSet, metaclass=abc.ABCMeta):
serializer = Serializer(notebook) serializer = Serializer(notebook)
return Response(serializer.data, status.HTTP_200_OK) return Response(serializer.data, status.HTTP_200_OK)
@action(["PATCH"], detail=True)
def lock(self, request: Request, **kwargs):
"""
Lock the `Notebook`.
Note that this does nothing on the backend; it's only meant to be an indication for the frontend.
"""
notebook: Notebook = self.get_object()
if notebook.locked_by is None:
notebook.locked_by = self.request.user
notebook.save()
Serializer = notebook.get_access_serializer(request.user)
serializer = Serializer(notebook)
return Response(serializer.data, status.HTTP_200_OK)
@action(["PATCH"], detail=True)
def unlock(self, request: Request, **kwargs):
"""
Unlock the `Notebook`.
Note that this does nothing on the backend; it's only meant to be an indication for the frontend.
"""
notebook: Notebook = self.get_object()
notebook.locked_by = None
notebook.save()
Serializer = notebook.get_access_serializer(request.user)
serializer = Serializer(notebook)
return Response(serializer.data, status.HTTP_200_OK)
@action(["PATCH"], detail=True) @action(["PATCH"], detail=True)
def stop(self, request: Request, **kwargs): def stop(self, request: Request, **kwargs):
""" """

View file

@ -16,6 +16,7 @@ import {SophonDescriptionBox} from "./components/informative/SophonDescriptionBo
import {InstanceDescriptionBox} from "./components/instance/InstanceDescriptionBox" import {InstanceDescriptionBox} from "./components/instance/InstanceDescriptionBox"
import {InstanceFormBox} from "./components/instance/InstanceFormBox" import {InstanceFormBox} from "./components/instance/InstanceFormBox"
import {InstanceRouter} from "./components/instance/InstanceRouter" import {InstanceRouter} from "./components/instance/InstanceRouter"
import {NotebookCreateBox} from "./components/notebook/NotebookCreateBox"
import {NotebookListBox} from "./components/notebook/NotebookListBox" import {NotebookListBox} from "./components/notebook/NotebookListBox"
import {NotebookRouter} from "./components/notebook/NotebookRouter" import {NotebookRouter} from "./components/notebook/NotebookRouter"
import {DebugBox} from "./components/placeholder/DebugBox" import {DebugBox} from "./components/placeholder/DebugBox"
@ -28,6 +29,8 @@ import {AuthorizationProvider} from "./contexts/authorization"
import {CacheProvider} from "./contexts/cache" import {CacheProvider} from "./contexts/cache"
import {GroupProvider} from "./contexts/group" import {GroupProvider} from "./contexts/group"
import {InstanceProvider} from "./contexts/instance" import {InstanceProvider} from "./contexts/instance"
import {NotebookProvider} from "./contexts/notebook"
import {ProjectProvider} from "./contexts/project"
import {ThemeProvider} from "./contexts/theme" import {ThemeProvider} from "./contexts/theme"
@ -67,17 +70,23 @@ function App({..._}: RouteComponentProps) {
<ProjectCreateBox viewSet={viewSet}/> <ProjectCreateBox viewSet={viewSet}/>
</>} </>}
selectedRoute={({selection}) => <> selectedRoute={({selection}) => <>
<ResourceDescriptionBox resource={selection} icon={faProjectDiagram}/> <ProjectProvider resource={selection}>
<ProjectCreateBox resource={selection}/> <ResourceDescriptionBox resource={selection} icon={faProjectDiagram}/>
<NotebookRouter <NotebookRouter
projectPk={selection.value.slug} projectPk={selection.value.slug}
unselectedRoute={({viewSet}) => <> unselectedRoute={({viewSet}) => <>
<NotebookListBox viewSet={viewSet}/> <ProjectCreateBox resource={selection}/>
</>} <NotebookListBox viewSet={viewSet}/>
selectedRoute={(props) => <> <NotebookCreateBox viewSet={viewSet}/>
<DebugBox {...props}/> </>}
</>} selectedRoute={({selection}) => <>
/> <NotebookProvider resource={selection}>
<NotebookCreateBox resource={selection}/>
<DebugBox {...selection}/>
</NotebookProvider>
</>}
/>
</ProjectProvider>
</>} </>}
/> />
</GroupProvider> </GroupProvider>

View file

@ -0,0 +1,120 @@
import {Box, Details, Form, Idiomatic as I, useFormState} from "@steffo/bluelib-react"
import * as React from "react"
import {useAuthorizationContext} from "../../contexts/authorization"
import {useProjectContext} from "../../contexts/project"
import {ManagedResource, ManagedViewSet} from "../../hooks/useManagedViewSet"
import {SophonNotebook} from "../../types/SophonTypes"
import {Validators} from "../../utils/Validators"
export interface NotebookCreateBoxProps {
viewSet?: ManagedViewSet<SophonNotebook>,
resource?: ManagedResource<SophonNotebook>,
}
export function NotebookCreateBox({viewSet, resource}: NotebookCreateBoxProps): JSX.Element {
const authorization = useAuthorizationContext()
const project = useProjectContext()
const name =
useFormState<string>(
resource?.value.name ?? "",
Validators.notZeroLength,
)
const slug =
React.useMemo(
() => resource ? resource.value.slug : name.value.replaceAll(/[^A-Za-z0-9-]/g, "-").toLowerCase(),
[resource, name],
)
const image =
useFormState<string>(
resource?.value.container_image ?? "steffo45/jupyterlab-docker-sophon",
Validators.alwaysValid,
)
// TODO: Fix this
const applyChanges =
React.useCallback(
async () => {
if(!project) {
return
}
if(resource) {
await resource.update({
name: name.value,
slug: slug,
container_image: image.value,
project: project?.value.slug,
})
}
else {
await viewSet!.create({
name: name.value,
slug: slug,
container_image: image.value,
project: project?.value.slug,
})
}
},
[viewSet, resource, name, slug, image, project],
)
const canApply =
React.useMemo(
() => name.validity === true && Boolean(authorization?.state.user?.username),
[name, authorization],
)
const hasError =
React.useMemo(
() => viewSet?.operationError || resource?.error,
[viewSet, resource],
)
return (
<Box>
<Details>
<Details.Summary>
{resource ? <>Edit <I>{resource.value.name}</I></> : "Create a new notebook"}
</Details.Summary>
<Details.Content>
<Form>
<Form.Field
label={"Name"}
required={true}
{...name}
/>
<Form.Field
label={"Slug"}
required={true}
disabled={true}
value={slug}
validity={slug.length > 0 ? true : undefined}
/>
<Form.Select
label={"Image"}
options={{
"Python (Sophonic)": "steffo45/jupyterlab-docker-sophon",
}}
{...image}
/>
<Form.Row>
<Form.Button
type={"button"}
onClick={applyChanges}
disabled={!canApply}
builtinColor={hasError ? "red" : undefined}
>
{resource ? "Edit" : "Create"}
</Form.Button>
</Form.Row>
</Form>
</Details.Content>
</Details>
</Box>
)
}

View file

@ -0,0 +1,40 @@
import {faTrash} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import * as React from "react"
import {useAuthorizationContext} from "../../contexts/authorization"
import {useGroupContext} from "../../contexts/group"
import {ManagedResource} from "../../hooks/useManagedViewSet"
import {SophonNotebook} from "../../types/SophonTypes"
import {SafetyButton} from "../elements/SafetyButton"
export interface NotebookDeleteButtonProps {
resource: ManagedResource<SophonNotebook>,
}
export function NotebookDeleteButton({resource}: NotebookDeleteButtonProps): JSX.Element | null {
const authorization = useAuthorizationContext()
const group = useGroupContext()
if(!authorization) {
return null
}
if(!group) {
return null
}
if(!authorization.state.user) {
return null
}
if(!(
group.value.members.includes(authorization.state.user.id) || group.value.owner === authorization.state.user.id
)) {
return null
}
return (
<SafetyButton timeout={3} onClick={() => resource.destroy()} disabled={resource.busy}>
<FontAwesomeIcon icon={faTrash} spin={resource.busy}/>&nbsp;Delete
</SafetyButton>
)
}

View file

@ -0,0 +1,43 @@
import {faLock} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {Button} from "@steffo/bluelib-react"
import * as React from "react"
import {useAuthorizationContext} from "../../contexts/authorization"
import {useGroupContext} from "../../contexts/group"
import {ManagedResource} from "../../hooks/useManagedViewSet"
import {SophonNotebook} from "../../types/SophonTypes"
export interface NotebookLockButtonProps {
resource: ManagedResource<SophonNotebook>,
}
export function NotebookLockButton({resource}: NotebookLockButtonProps): JSX.Element | null {
const authorization = useAuthorizationContext()
const group = useGroupContext()
if(!authorization) {
return null
}
if(!group) {
return null
}
if(!authorization.state.user) {
return null
}
if(!(
group.value.members.includes(authorization.state.user.id) || group.value.owner === authorization.state.user.id
)) {
return null
}
if(resource.value.locked_by) {
return null
}
return (
<Button onClick={() => resource.action("PATCH", "lock", {})} disabled={resource.busy}>
<FontAwesomeIcon icon={faLock} spin={resource.busy}/>&nbsp;Lock
</Button>
)
}

View file

@ -1,10 +1,16 @@
import {faBook} from "@fortawesome/free-solid-svg-icons" import {faBook} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import * as React from "react" import * as React from "react"
import {useCacheContext} from "../../contexts/cache"
import {ManagedResource} from "../../hooks/useManagedViewSet" import {ManagedResource} from "../../hooks/useManagedViewSet"
import {SophonNotebook} from "../../types/SophonTypes" import {SophonNotebook} from "../../types/SophonTypes"
import {Link} from "../elements/Link" import {Link} from "../elements/Link"
import {ResourcePanel} from "../elements/ResourcePanel" import {ResourcePanel} from "../elements/ResourcePanel"
import {NotebookDeleteButton} from "./NotebookDeleteButton"
import {NotebookLockButton} from "./NotebookLockButton"
import {NotebookStartButton} from "./NotebookStartButton"
import {NotebookStopButton} from "./NotebookStopButton"
import {NotebookUnlockButton} from "./NotebookUnlockButton"
export interface NotebookResourcePanelProps { export interface NotebookResourcePanelProps {
@ -13,6 +19,9 @@ export interface NotebookResourcePanelProps {
export function NotebookResourcePanel({resource}: NotebookResourcePanelProps): JSX.Element { export function NotebookResourcePanel({resource}: NotebookResourcePanelProps): JSX.Element {
const cache = useCacheContext()
const locked_by = cache?.getUserById(resource.value.locked_by)?.value.username
return ( return (
<ResourcePanel> <ResourcePanel>
@ -25,10 +34,20 @@ export function NotebookResourcePanel({resource}: NotebookResourcePanelProps): J
</Link> </Link>
</ResourcePanel.Name> </ResourcePanel.Name>
<ResourcePanel.Text> <ResourcePanel.Text>
{
resource.value.locked_by
?
`Locked by ${locked_by}`
:
null
}
</ResourcePanel.Text> </ResourcePanel.Text>
<ResourcePanel.Buttons> <ResourcePanel.Buttons>
<NotebookStopButton resource={resource}/>
<NotebookStartButton resource={resource}/>
<NotebookUnlockButton resource={resource}/>
<NotebookLockButton resource={resource}/>
<NotebookDeleteButton resource={resource}/>
</ResourcePanel.Buttons> </ResourcePanel.Buttons>
</ResourcePanel> </ResourcePanel>
) )

View file

@ -16,7 +16,7 @@ export function NotebookRouter({projectPk, ...props}: ProjectRouterProps): JSX.E
return ( return (
<ViewSetRouter <ViewSetRouter
{...props} {...props}
viewSet={useManagedViewSet<SophonResearchProject>(`/api/notebooks/by-project/${projectPk}`, "slug")} viewSet={useManagedViewSet<SophonResearchProject>(`/api/notebooks/by-project/${projectPk}/`, "slug")}
pathSegment={"notebook"} pathSegment={"notebook"}
pkKey={"slug"} pkKey={"slug"}
/> />

View file

@ -0,0 +1,46 @@
import {faLightbulb} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {Button} from "@steffo/bluelib-react"
import * as React from "react"
import {useAuthorizationContext} from "../../contexts/authorization"
import {useGroupContext} from "../../contexts/group"
import {ManagedResource} from "../../hooks/useManagedViewSet"
import {SophonNotebook} from "../../types/SophonTypes"
export interface NotebookStartButtonProps {
resource: ManagedResource<SophonNotebook>,
}
export function NotebookStartButton({resource}: NotebookStartButtonProps): JSX.Element | null {
const authorization = useAuthorizationContext()
const group = useGroupContext()
if(!authorization) {
return null
}
if(!group) {
return null
}
if(!authorization.state.user) {
return null
}
if(!(
group.value.members.includes(authorization.state.user.id) || group.value.owner === authorization.state.user.id
)) {
return null
}
if(resource.value.is_running) {
return null
}
if(resource.value.locked_by) {
return null
}
return (
<Button onClick={() => resource.action("PATCH", "start", {})} disabled={resource.busy}>
<FontAwesomeIcon icon={faLightbulb} spin={resource.busy}/>&nbsp;Start
</Button>
)
}

View file

@ -0,0 +1,46 @@
import {faLightbulb} from "@fortawesome/free-regular-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {Button} from "@steffo/bluelib-react"
import * as React from "react"
import {useAuthorizationContext} from "../../contexts/authorization"
import {useGroupContext} from "../../contexts/group"
import {ManagedResource} from "../../hooks/useManagedViewSet"
import {SophonNotebook} from "../../types/SophonTypes"
export interface NotebookStopButtonProps {
resource: ManagedResource<SophonNotebook>,
}
export function NotebookStopButton({resource}: NotebookStopButtonProps): JSX.Element | null {
const authorization = useAuthorizationContext()
const group = useGroupContext()
if(!authorization) {
return null
}
if(!group) {
return null
}
if(!authorization.state.user) {
return null
}
if(!(
group.value.members.includes(authorization.state.user.id) || group.value.owner === authorization.state.user.id
)) {
return null
}
if(!resource.value.is_running) {
return null
}
if(resource.value.locked_by) {
return null
}
return (
<Button onClick={() => resource.action("PATCH", "stop", {})} disabled={resource.busy}>
<FontAwesomeIcon icon={faLightbulb} spin={resource.busy}/>&nbsp;Stop
</Button>
)
}

View file

@ -0,0 +1,52 @@
import {faLockOpen} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {Button} from "@steffo/bluelib-react"
import * as React from "react"
import {useAuthorizationContext} from "../../contexts/authorization"
import {useGroupContext} from "../../contexts/group"
import {ManagedResource} from "../../hooks/useManagedViewSet"
import {SophonNotebook} from "../../types/SophonTypes"
import {SafetyButton} from "../elements/SafetyButton"
export interface NotebookUnlockButtonProps {
resource: ManagedResource<SophonNotebook>,
}
export function NotebookUnlockButton({resource}: NotebookUnlockButtonProps): JSX.Element | null {
const authorization = useAuthorizationContext()
const group = useGroupContext()
if(!authorization) {
return null
}
if(!group) {
return null
}
if(!authorization.state.user) {
return null
}
if(!(
group.value.members.includes(authorization.state.user.id) || group.value.owner === authorization.state.user.id
)) {
return null
}
if(!resource.value.locked_by) {
return null
}
if(resource.value.locked_by === authorization.state.user.id) {
return (
<Button onClick={() => resource.action("PATCH", "unlock", {})} disabled={resource.busy}>
<FontAwesomeIcon icon={faLockOpen} spin={resource.busy}/>&nbsp;Unlock
</Button>
)
}
return (
<SafetyButton timeout={3} onClick={() => resource.action("PATCH", "unlock", {})} disabled={resource.busy}>
<FontAwesomeIcon icon={faLockOpen} spin={resource.busy}/>&nbsp;Unlock
</SafetyButton>
)
}

View file

@ -65,7 +65,7 @@ export function ProjectCreateBox({viewSet, resource}: ProjectCreateBoxProps): JS
return true return true
} }
}, },
[authorization, resource], [authorization, group, resource],
) )
const applyChanges = const applyChanges =
@ -96,7 +96,7 @@ export function ProjectCreateBox({viewSet, resource}: ProjectCreateBoxProps): JS
const canApply = const canApply =
React.useMemo( React.useMemo(
() => name.validity === true && visibility.validity === true && Boolean(authorization?.state.user?.username) && group, () => name.validity === true && visibility.validity === true && Boolean(authorization?.state.user?.username) && group,
[name, visibility, authorization], [name, visibility, authorization, group],
) )
const hasError = const hasError =

View file

@ -52,8 +52,16 @@ export function CacheProvider({children}: WithChildren): JSX.Element {
const getUserById = const getUserById =
React.useCallback( React.useCallback(
(id: number) => usersIdMap?.[id.toString()], (id: number) => {
[usersIdMap], if(!id) {
return undefined
}
if(!usersIdMap) {
return undefined
}
return usersIdMap[id.toString()]
},
[usersIdMap]
) )
return <CacheContext.Provider value={{users, getUserById}} children={children}/> return <CacheContext.Provider value={{users, getUserById}} children={children}/>

View file

@ -0,0 +1,21 @@
import * as React from "react"
import {ManagedResource} from "../hooks/useManagedViewSet"
import {WithChildren, WithResource} from "../types/ExtraTypes"
import {SophonNotebook} from "../types/SophonTypes"
const notebookContext = React.createContext<ManagedResource<SophonNotebook> | undefined>(undefined)
const NotebookContext = notebookContext
/**
* Hook to access the {@link notebookContext}.
*/
export function useNotebookContext(): ManagedResource<SophonNotebook> | undefined {
return React.useContext(notebookContext)
}
export function NotebookProvider({resource, children}: WithResource<SophonNotebook> & WithChildren): JSX.Element {
return <NotebookContext.Provider value={resource} children={children}/>
}

View file

@ -0,0 +1,21 @@
import * as React from "react"
import {ManagedResource} from "../hooks/useManagedViewSet"
import {WithChildren, WithResource} from "../types/ExtraTypes"
import {SophonResearchProject} from "../types/SophonTypes"
const projectContext = React.createContext<ManagedResource<SophonResearchProject> | undefined>(undefined)
const ProjectContext = projectContext
/**
* Hook to access the {@link projectContext}.
*/
export function useProjectContext(): ManagedResource<SophonResearchProject> | undefined {
return React.useContext(projectContext)
}
export function ProjectProvider({resource, children}: WithResource<SophonResearchProject> & WithChildren): JSX.Element {
return <ProjectContext.Provider value={resource} children={children}/>
}