From c668249754723a73f5487e35081a40905cb68bd6 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Thu, 10 Oct 2024 14:19:34 -0300 Subject: [PATCH 1/4] add setting to control whether to run previous unexecuted blocks --- .../workspace/documents/document/index.ts | 2 + .../workspace/documents/document/settings.ts | 56 +++++++++ apps/web/src/components/EllipsisDropdown.tsx | 10 ++ apps/web/src/components/Files.tsx | 4 +- apps/web/src/components/PageSettingsPanel.tsx | 110 ++++++++++++++++++ .../src/components/PrivateDocumentPage.tsx | 16 ++- apps/web/src/components/v2Editor/index.tsx | 13 ++- apps/web/src/hooks/useDocument.ts | 25 +++- apps/web/src/hooks/useDocuments.tsx | 51 ++++++++ .../migration.sql | 9 ++ packages/database/prisma/schema.prisma | 4 +- 11 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 apps/api/src/v1/workspaces/workspace/documents/document/settings.ts create mode 100644 apps/web/src/components/PageSettingsPanel.tsx create mode 100644 packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql diff --git a/apps/api/src/v1/workspaces/workspace/documents/document/index.ts b/apps/api/src/v1/workspaces/workspace/documents/document/index.ts index 8463e048..9cf2a297 100644 --- a/apps/api/src/v1/workspaces/workspace/documents/document/index.ts +++ b/apps/api/src/v1/workspaces/workspace/documents/document/index.ts @@ -26,6 +26,7 @@ import { import inputsRouter from './inputs.js' import publishRouter from './publish.js' import { canUpdateWorkspace } from '../../../../../auth/token.js' +import settingsRouter from './settings.js' export default function documentRouter(socketServer: IOServer) { const router = Router({ mergeParams: true }) @@ -256,6 +257,7 @@ export default function documentRouter(socketServer: IOServer) { router.use('/icon', canUpdateWorkspace, iconRouter) router.use('/inputs', canUpdateWorkspace, inputsRouter) router.use('/publish', canUpdateWorkspace, publishRouter(socketServer)) + router.use('/settings', canUpdateWorkspace, settingsRouter(socketServer)) return router } diff --git a/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts b/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts new file mode 100644 index 00000000..9e309b8e --- /dev/null +++ b/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts @@ -0,0 +1,56 @@ +import { Router } from 'express' +import { getParam } from '../../../../../utils/express.js' +import { z } from 'zod' +import { IOServer } from '../../../../../websocket/index.js' +import prisma from '@briefer/database' +import { broadcastDocument } from '../../../../../websocket/workspace/documents.js' + +const DocumentSettings = z.object({ + runUnexecutedBlocks: z.boolean(), +}) + +export default function settingsRouter(socketServer: IOServer) { + const publishRouter = Router({ mergeParams: true }) + + publishRouter.put('/', async (req, res) => { + const workspaceId = getParam(req, 'workspaceId') + const documentId = getParam(req, 'documentId') + const body = DocumentSettings.safeParse(req.body) + if (!body.success) { + res.status(400).json({ reason: 'invalid-payload' }) + return + } + + const runUnexecutedBlocks = body.data.runUnexecutedBlocks + + try { + await prisma().$transaction(async (tx) => { + const doc = await tx.document.update({ + where: { id: documentId }, + data: { runUnexecutedBlocks }, + }) + + if (!doc) { + res.status(404).end() + return + } + }) + + await broadcastDocument(socketServer, workspaceId, documentId) + + res.sendStatus(204) + } catch (err) { + req.log.error( + { + workspaceId, + documentId, + err, + }, + 'Failed to publish document' + ) + res.status(500).end() + } + }) + + return publishRouter +} diff --git a/apps/web/src/components/EllipsisDropdown.tsx b/apps/web/src/components/EllipsisDropdown.tsx index e8744e78..f3278863 100644 --- a/apps/web/src/components/EllipsisDropdown.tsx +++ b/apps/web/src/components/EllipsisDropdown.tsx @@ -2,6 +2,7 @@ import { BookOpenIcon, ClockIcon, CodeBracketSquareIcon, + Cog6ToothIcon, MapIcon, } from '@heroicons/react/24/outline' import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid' @@ -21,6 +22,7 @@ interface Props { onToggleFiles?: () => void onToggleSchemaExplorer?: () => void onToggleShortcuts?: () => void + onTogglePageSettings?: () => void onToggleReusableComponents?: () => void isViewer: boolean isDeleted: boolean @@ -123,6 +125,14 @@ function EllipsisDropdown(props: Props) { onClick={props.onToggleShortcuts} /> )} + + {props.onTogglePageSettings && ( + } + text="Page settings" + onClick={props.onTogglePageSettings} + /> + )} diff --git a/apps/web/src/components/Files.tsx b/apps/web/src/components/Files.tsx index f35862f9..192d4d95 100644 --- a/apps/web/src/components/Files.tsx +++ b/apps/web/src/components/Files.tsx @@ -86,7 +86,7 @@ file` return } - requestRun(pythonBlock, blocks, layout, environmentStartedAt, false) + requestRun(pythonBlock, blocks, layout, environmentStartedAt, true) }, [props.yDoc, environmentStartedAt] ) @@ -131,7 +131,7 @@ file` return } - requestRun(sqlBlock, blocks, layout, environmentStartedAt, false) + requestRun(sqlBlock, blocks, layout, environmentStartedAt, true) }, [props.yDoc, environmentStartedAt] ) diff --git a/apps/web/src/components/PageSettingsPanel.tsx b/apps/web/src/components/PageSettingsPanel.tsx new file mode 100644 index 00000000..521d261e --- /dev/null +++ b/apps/web/src/components/PageSettingsPanel.tsx @@ -0,0 +1,110 @@ +import * as Y from 'yjs' +import { useState } from 'react' +import { Switch } from '@headlessui/react' +import { ChevronDoubleRightIcon } from '@heroicons/react/24/outline' +import { Transition } from '@headlessui/react' +import clsx from 'clsx' +import useDocument from '@/hooks/useDocument' + +interface Props { + workspaceId: string + documentId: string + visible: boolean + onHide: () => void + yDoc?: Y.Doc +} + +export default function PageSettingsPanel(props: Props) { + const [{ document }, api] = useDocument(props.workspaceId, props.documentId) + + console.log('runUnexecutedBlocks', document?.runUnexecutedBlocks) + return ( + <> + + +
+
+
+

+ Page settings +

+

+ { + "Configure this page's behavior and default visualization mode." + } +

+
+
+
+ +
+
+
+ + ) +} + +type PageSettingToggleProps = { + name: string + description: string + enabled: boolean + onToggle: () => void + disabled?: boolean +} + +export function PageSettingToggle(props: PageSettingToggleProps) { + return ( + + + + {props.name} + + + + + {props.description} + + ) +} diff --git a/apps/web/src/components/PrivateDocumentPage.tsx b/apps/web/src/components/PrivateDocumentPage.tsx index 3be7e7c0..750223eb 100644 --- a/apps/web/src/components/PrivateDocumentPage.tsx +++ b/apps/web/src/components/PrivateDocumentPage.tsx @@ -32,7 +32,7 @@ import { EditorAwarenessProvider } from '@/hooks/useEditorAwareness' import ShortcutsModal from './ShortcutsModal' import { NEXT_PUBLIC_PUBLIC_URL } from '@/utils/env' import ReusableComponents from './ReusableComponents' -import { SaveConfirmationModal } from './ReusableComponents' +import PageSettingsPanel from './PageSettingsPanel' // this is needed because this component only works with the browser const V2Editor = dynamic(() => import('@/components/v2Editor'), { @@ -94,6 +94,7 @@ function PrivateDocumentPageInner( | { _tag: 'schemaExplorer'; dataSourceId: string | null } | { _tag: 'shortcuts' } | { _tag: 'reusableComponents' } + | { _tag: 'pageSettings' } | null >(null) @@ -156,6 +157,12 @@ function PrivateDocumentPageInner( setSelectedSidebar((v) => (v?._tag === 'files' ? null : { _tag: 'files' })) }, [setSelectedSidebar]) + const onTogglePageSettings = useCallback(() => { + setSelectedSidebar((v) => + v?._tag === 'pageSettings' ? null : { _tag: 'pageSettings' } + ) + }, [setSelectedSidebar]) + const router = useRouter() const copyLink = useMemo( () => @@ -308,6 +315,7 @@ function PrivateDocumentPageInner( onToggleSchemaExplorer={onToggleSchemaExplorerEllipsis} onToggleReusableComponents={onToggleReusableComponents} onToggleShortcuts={onToggleShortcuts} + onTogglePageSettings={onTogglePageSettings} isViewer={isViewer} isDeleted={isDeleted} isFullScreen={isFullScreen} @@ -400,6 +408,12 @@ function PrivateDocumentPageInner( onHide={onHideSidebar} yDoc={yDoc} /> + )} diff --git a/apps/web/src/components/v2Editor/index.tsx b/apps/web/src/components/v2Editor/index.tsx index 22cb2d7e..f0be98da 100644 --- a/apps/web/src/components/v2Editor/index.tsx +++ b/apps/web/src/components/v2Editor/index.tsx @@ -476,11 +476,16 @@ const DraggableTabbedBlock = (props: { blocks.value, layout.value, environmentStartedAt, - false, + !props.document.runUnexecutedBlocks, customCallback ) }, - [blocks.value, layout.value, environmentStartedAt] + [ + blocks.value, + layout.value, + props.document.runUnexecutedBlocks, + environmentStartedAt, + ] ) const onTry = useCallback( @@ -537,7 +542,7 @@ file` blocks.value, layout.value, environmentStartedAt, - false + true ) }, [blocks, layout, environmentStartedAt] @@ -581,7 +586,7 @@ file` blocks.value, layout.value, environmentStartedAt, - false + true ) }, [] diff --git a/apps/web/src/hooks/useDocument.ts b/apps/web/src/hooks/useDocument.ts index d4767b6d..5937cbf4 100644 --- a/apps/web/src/hooks/useDocument.ts +++ b/apps/web/src/hooks/useDocument.ts @@ -5,6 +5,7 @@ import { useDocuments } from './useDocuments' type API = { setIcon: (icon: string) => Promise publish: () => Promise + toggleRunUnexecutedBlocks: () => Promise } type UseDocument = [ @@ -13,7 +14,7 @@ type UseDocument = [ loading: boolean publishing: boolean }, - API, + API ] function useDocument(workspaceId: string, documentId: string): UseDocument { @@ -23,6 +24,8 @@ function useDocument(workspaceId: string, documentId: string): UseDocument { [documents, documentId] ) + const currRunUnexecutedBlocks = document?.runUnexecutedBlocks ?? false + const setIcon = useCallback( (icon: string) => api.setIcon(documentId, icon), [api, documentId] @@ -40,12 +43,28 @@ function useDocument(workspaceId: string, documentId: string): UseDocument { } }, [workspaceId, documentId, api.publish]) + const toggleRunUnexecutedBlocks = useCallback(async () => { + const newRunUnexecutedBlocks = !currRunUnexecutedBlocks + try { + await api.updateDocumentSettings(documentId, { + runUnexecutedBlocks: newRunUnexecutedBlocks, + }) + } catch (err) { + alert('Failed to update document settings') + } + }, [ + workspaceId, + documentId, + currRunUnexecutedBlocks, + api.updateDocumentSettings, + ]) + return useMemo( () => [ { document, loading, publishing }, - { setIcon, publish }, + { setIcon, publish, toggleRunUnexecutedBlocks }, ], - [loading, setIcon, publish] + [loading, setIcon, publish, toggleRunUnexecutedBlocks] ) } diff --git a/apps/web/src/hooks/useDocuments.tsx b/apps/web/src/hooks/useDocuments.tsx index 6b85fbd0..d588ca67 100644 --- a/apps/web/src/hooks/useDocuments.tsx +++ b/apps/web/src/hooks/useDocuments.tsx @@ -202,6 +202,10 @@ type API = { parentId: string | null, orderIndex: number ) => Promise + updateDocumentSettings: ( + id: string, + settings: { runUnexecutedBlocks: boolean } + ) => Promise publish: (id: string) => Promise } @@ -731,6 +735,51 @@ export function useDocuments(workspaceId: string): UseDocuments { [documents, workspaceId, state, setState] ) + const updateDocumentSettings = useCallback( + async (id: string, settings: { runUnexecutedBlocks: boolean }) => { + const document = documents.find((doc) => doc.id === id) + if (!document) { + return + } + + setState((s) => { + const { loading, documents } = s.get(workspaceId) ?? { + loading: true, + documents: List(), + } + + return s.set(workspaceId, { + loading, + documents: documents.map((doc) => + doc.id === id + ? { + ...doc, + ...settings, + } + : doc + ), + }) + }) + + const res = await fetch( + `${NEXT_PUBLIC_API_URL()}/v1/workspaces/${workspaceId}/documents/${id}/settings`, + { + credentials: 'include', + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(settings), + } + ) + + if (!res.ok) { + throw new Error(`Error changing settings for Document(${id})`) + } + }, + [documents, workspaceId, state, setState] + ) + return useMemo( () => [ { loading, documents }, @@ -742,6 +791,7 @@ export function useDocuments(workspaceId: string): UseDocuments { setIcon, updateParent, publish, + updateDocumentSettings, }, ], [ @@ -754,6 +804,7 @@ export function useDocuments(workspaceId: string): UseDocuments { setIcon, updateParent, publish, + updateDocumentSettings, ] ) } diff --git a/packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql b/packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql new file mode 100644 index 00000000..3d3a9767 --- /dev/null +++ b/packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `runUnexecutedblocks` on the `Document` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Document" DROP COLUMN "runUnexecutedblocks", +ADD COLUMN "runUnexecutedBlocks" BOOLEAN NOT NULL DEFAULT true; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 60b11d3d..1dbe58d7 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -35,6 +35,8 @@ model Document { Favorite Favorite[] executionSchedules ExecutionSchedule[] reusableComponents ReusableComponent[] + + runUnexecutedBlocks Boolean @default(true) } model YjsDocument { @@ -204,7 +206,7 @@ model SnowflakeDataSource { password String warehouse String database String - region String @default("us-east-1") + region String @default("us-east-1") notes String structure String? isDemo Boolean @default(false) From 4ad3df80248f55a43e140411594a58a4e75686c4 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Thu, 10 Oct 2024 21:02:47 -0300 Subject: [PATCH 2/4] fix migration for unexecuted blocks --- .../migration.sql | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql b/packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql index 3d3a9767..83ea7edc 100644 --- a/packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql +++ b/packages/database/prisma/migrations/20241010164405_add_run_unexecuted_setting/migration.sql @@ -1,9 +1,2 @@ -/* - Warnings: - - - You are about to drop the column `runUnexecutedblocks` on the `Document` table. All the data in the column will be lost. - -*/ -- AlterTable -ALTER TABLE "Document" DROP COLUMN "runUnexecutedblocks", -ADD COLUMN "runUnexecutedBlocks" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "Document" ADD COLUMN "runUnexecutedBlocks" BOOLEAN NOT NULL DEFAULT true; From afc58657bf23329197bdb763f9f158fa6a3823d2 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Thu, 10 Oct 2024 21:05:26 -0300 Subject: [PATCH 3/4] fix bugs brought over from publishRouter to settingsRouter --- .../workspace/documents/document/settings.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts b/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts index 9e309b8e..a9b770f3 100644 --- a/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts +++ b/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts @@ -10,9 +10,9 @@ const DocumentSettings = z.object({ }) export default function settingsRouter(socketServer: IOServer) { - const publishRouter = Router({ mergeParams: true }) + const settingsRouter = Router({ mergeParams: true }) - publishRouter.put('/', async (req, res) => { + settingsRouter.put('/', async (req, res) => { const workspaceId = getParam(req, 'workspaceId') const documentId = getParam(req, 'documentId') const body = DocumentSettings.safeParse(req.body) @@ -24,18 +24,16 @@ export default function settingsRouter(socketServer: IOServer) { const runUnexecutedBlocks = body.data.runUnexecutedBlocks try { - await prisma().$transaction(async (tx) => { - const doc = await tx.document.update({ - where: { id: documentId }, - data: { runUnexecutedBlocks }, - }) - - if (!doc) { - res.status(404).end() - return - } + const doc = await prisma().document.update({ + where: { id: documentId }, + data: { runUnexecutedBlocks }, }) + if (!doc) { + res.status(404).end() + return + } + await broadcastDocument(socketServer, workspaceId, documentId) res.sendStatus(204) @@ -46,11 +44,11 @@ export default function settingsRouter(socketServer: IOServer) { documentId, err, }, - 'Failed to publish document' + 'Failed to update document settings' ) res.status(500).end() } }) - return publishRouter + return settingsRouter } From 01ea4ea64b494de815e016c0a4cbddc6ac0753de Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Thu, 10 Oct 2024 21:11:46 -0300 Subject: [PATCH 4/4] send 404 for doc not found when updating doc settings --- .../workspace/documents/document/settings.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts b/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts index a9b770f3..dc1efc9f 100644 --- a/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts +++ b/apps/api/src/v1/workspaces/workspace/documents/document/settings.ts @@ -2,7 +2,7 @@ import { Router } from 'express' import { getParam } from '../../../../../utils/express.js' import { z } from 'zod' import { IOServer } from '../../../../../websocket/index.js' -import prisma from '@briefer/database' +import prisma, { recoverFromNotFound } from '@briefer/database' import { broadcastDocument } from '../../../../../websocket/workspace/documents.js' const DocumentSettings = z.object({ @@ -24,11 +24,12 @@ export default function settingsRouter(socketServer: IOServer) { const runUnexecutedBlocks = body.data.runUnexecutedBlocks try { - const doc = await prisma().document.update({ - where: { id: documentId }, - data: { runUnexecutedBlocks }, - }) - + const doc = await recoverFromNotFound( + prisma().document.update({ + where: { id: documentId }, + data: { runUnexecutedBlocks }, + }) + ) if (!doc) { res.status(404).end() return