diff --git a/package.json b/package.json index 80cf4ca..79653ec 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-easy-sort": "^1.5.3", + "react-grid-layout": "1.4.2", + "react-resizable": "^3.0.5", "react-router": "^6.15.0", "react-router-dom": "^6.15.0", "request-ip": "^3.3.0", @@ -99,6 +101,8 @@ "@types/ping": "^0.4.2", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "@types/react-grid-layout": "^1.3.5", + "@types/react-resizable": "^3.0.7", "@types/request-ip": "^0.0.38", "@types/swagger-ui-express": "^4.1.5", "@types/tar": "^6.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50dc511..6532210 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,6 +139,12 @@ dependencies: react-easy-sort: specifier: ^1.5.3 version: 1.5.3(react-dom@18.2.0)(react@18.2.0) + react-grid-layout: + specifier: 1.4.2 + version: 1.4.2(react-dom@18.2.0)(react@18.2.0) + react-resizable: + specifier: ^3.0.5 + version: 3.0.5(react-dom@18.2.0)(react@18.2.0) react-router: specifier: ^6.15.0 version: 6.15.0(react@18.2.0) @@ -234,6 +240,12 @@ devDependencies: '@types/react-dom': specifier: ^18.2.7 version: 18.2.7 + '@types/react-grid-layout': + specifier: ^1.3.5 + version: 1.3.5 + '@types/react-resizable': + specifier: ^3.0.7 + version: 3.0.7 '@types/request-ip': specifier: ^0.0.38 version: 0.0.38 @@ -2199,6 +2211,18 @@ packages: '@types/react': 18.2.21 dev: true + /@types/react-grid-layout@1.3.5: + resolution: {integrity: sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==} + dependencies: + '@types/react': 18.2.21 + dev: true + + /@types/react-resizable@3.0.7: + resolution: {integrity: sha512-V4N7/xDUME+cxKya/A73MmFrHofTupVdE45boRxeA8HL4Q5pJh3AuG0FWCEy2GB84unIMSRISyEAS/GHWum9EQ==} + dependencies: + '@types/react': 18.2.21 + dev: true + /@types/react@18.2.21: resolution: {integrity: sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==} dependencies: @@ -2794,6 +2818,11 @@ packages: wrap-ansi: 7.0.0 dev: false + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + /clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -3647,6 +3676,10 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: false + /fast-equals@4.0.3: + resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==} + dev: false + /fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} dev: false @@ -6007,6 +6040,18 @@ packages: scheduler: 0.23.0 dev: false + /react-draggable@4.4.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-easy-sort@1.5.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-d5DVqmlqGfpRcE2blMkZ/H8AvsY6KZmUG5nOeUrV5INWMbqDIXTRBkkBnJvNOQT6mupAjl4wokgQcur5zRMYxw==} engines: {node: '>=16'} @@ -6020,6 +6065,22 @@ packages: tslib: 2.0.1 dev: false + /react-grid-layout@1.4.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-LLOZogtw5XNHbdCquKQRG/Dspjyfelk+kE9DKRbLUl3UArFRQu/IiH+aPcjh+wSkSHUjf+Rv32ueEYigbGzRLQ==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + dependencies: + clsx: 2.0.0 + fast-equals: 4.0.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0) + react-resizable: 3.0.5(react-dom@18.2.0)(react@18.2.0) + resize-observer-polyfill: 1.5.1 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false @@ -6033,6 +6094,18 @@ packages: engines: {node: '>=0.10.0'} dev: true + /react-resizable@3.0.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==} + peerDependencies: + react: '>= 16.3' + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react-dom + dev: false + /react-resize-detector@7.1.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==} peerDependencies: diff --git a/prisma/migrations/20231112152240_add_dashboard_layout/migration.sql b/prisma/migrations/20231112152240_add_dashboard_layout/migration.sql new file mode 100644 index 0000000..4d43c8a --- /dev/null +++ b/prisma/migrations/20231112152240_add_dashboard_layout/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Workspace" ADD COLUMN "dashboardLayout" JSON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b1a5810..94e26f7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,11 +23,12 @@ model User { } model Workspace { - id String @id @unique @default(cuid()) @db.VarChar(30) - name String @db.VarChar(100) - dashboardOrder String[] - createdAt DateTime @default(now()) @db.Timestamptz(6) - updatedAt DateTime @updatedAt @db.Timestamptz(6) + id String @id @unique @default(cuid()) @db.VarChar(30) + name String @db.VarChar(100) + dashboardOrder String[] + dashboardLayout Json? @db.Json + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) users WorkspacesOnUsers[] websites Website[] diff --git a/src/client/App.tsx b/src/client/App.tsx index 8a71b66..20a1f22 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,6 +1,6 @@ import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom'; import { Layout } from './pages/Layout'; -import { Dashboard } from './pages/Dashboard'; +import { DashboardPage } from './pages/Dashboard'; import { Login } from './pages/Login'; import { SettingsPage } from './pages/Settings'; import { Servers } from './pages/Servers'; @@ -26,7 +26,7 @@ export const AppRoutes: React.FC = React.memo(() => { {info ? ( }> - } /> + } /> } /> } /> } /> diff --git a/src/client/components/dashboard/AddButton.tsx b/src/client/components/dashboard/AddButton.tsx new file mode 100644 index 0000000..71f5ffc --- /dev/null +++ b/src/client/components/dashboard/AddButton.tsx @@ -0,0 +1,56 @@ +import { Button, Dropdown, Space } from 'antd'; +import React from 'react'; +import { trpc } from '../../api/trpc'; +import { useCurrentWorkspaceId } from '../../store/user'; +import { useDashboardStore } from '../../store/dashboard'; +import { DownOutlined } from '@ant-design/icons'; + +export const DashboardItemAddButton: React.FC = React.memo(() => { + const workspaceId = useCurrentWorkspaceId(); + const { data: websites = [], isLoading } = trpc.website.all.useQuery({ + workspaceId, + }); + const { addItem } = useDashboardStore(); + + return ( +
+ ({ + key: `website#${website.id}`, + label: website.name, + children: [ + { + key: `website#${website.id}#overview`, + label: 'overview', + onClick: () => { + addItem( + 'websiteOverview', + website.id, + `${website.name}'s Overview` + ); + }, + }, + ], + })), + }, + ], + }} + > + + +
+ ); +}); +DashboardItemAddButton.displayName = 'DashboardItemAddButton'; diff --git a/src/client/components/dashboard/Dashboard.tsx b/src/client/components/dashboard/Dashboard.tsx new file mode 100644 index 0000000..6de6ca3 --- /dev/null +++ b/src/client/components/dashboard/Dashboard.tsx @@ -0,0 +1,90 @@ +import React, { useEffect } from 'react'; +import { DashboardGrid } from './Grid'; +import { DashboardItemAddButton } from './AddButton'; +import { defaultBlankLayouts, useDashboardStore } from '../../store/dashboard'; +import { useEvent } from '../../hooks/useEvent'; +import { Layouts } from 'react-grid-layout'; +import { Button, Empty, message } from 'antd'; +import { DateFilter } from '../DateFilter'; +import { trpc } from '../../api/trpc'; +import { useCurrentWorkspace, useCurrentWorkspaceId } from '../../store/user'; + +export const Dashboard: React.FC = React.memo(() => { + const { isEditMode, switchEditMode, layouts, items } = useDashboardStore(); + const mutation = trpc.workspace.saveDashboardLayout.useMutation(); + const workspaceId = useCurrentWorkspaceId(); + const workspace = useCurrentWorkspace(); + + useEffect(() => { + // Init on mount + const { items = [], layouts = defaultBlankLayouts } = + workspace.dashboardLayout ?? {}; + + useDashboardStore.setState({ + items, + layouts, + }); + }, []); + + const handleChangeLayouts = useEvent((layouts: Layouts) => { + useDashboardStore.setState({ + layouts, + }); + }); + + const handleSaveDashboardLayout = useEvent(async () => { + await mutation.mutateAsync({ + workspaceId, + dashboardLayout: { + layouts, + items, + }, + }); + switchEditMode(); + message.success('Layout saved success'); + }); + + return ( +
+
+ {isEditMode ? ( + <> + + + + ) : ( + <> + + + + )} +
+ + + {items.length === 0 && ( + + )} +
+ ); +}); +Dashboard.displayName = 'Dashboard'; diff --git a/src/client/components/dashboard/Grid.tsx b/src/client/components/dashboard/Grid.tsx new file mode 100644 index 0000000..bebd427 --- /dev/null +++ b/src/client/components/dashboard/Grid.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import { Layouts, Responsive, WidthProvider } from 'react-grid-layout'; +import clsx from 'clsx'; +import { DashboardGridItem } from './items'; +import { DashboardItem } from '../../store/dashboard'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; + +const ResponsiveGridLayout = WidthProvider(Responsive); + +interface DashboardGridProps { + isEditMode: boolean; + items: DashboardItem[]; + layouts: Layouts; + onChangeLayouts: (layouts: Layouts) => void; +} +export const DashboardGrid: React.FC = React.memo( + (props) => { + const { layouts, onChangeLayouts, items, isEditMode } = props; + + return ( + { + onChangeLayouts(allLayouts); + }} + > + {items.map((item) => ( +
+ +
+ ))} +
+ ); + } +); +DashboardGrid.displayName = 'DashboardGrid'; diff --git a/src/client/components/dashboard/items/WebsiteOverviewItem.tsx b/src/client/components/dashboard/items/WebsiteOverviewItem.tsx new file mode 100644 index 0000000..7184219 --- /dev/null +++ b/src/client/components/dashboard/items/WebsiteOverviewItem.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { trpc } from '../../../api/trpc'; +import { useCurrentWorkspaceId } from '../../../store/user'; +import { Loading } from '../../Loading'; +import { NotFoundTip } from '../../NotFoundTip'; +import { WebsiteOverview } from '../../website/WebsiteOverview'; +import { Button } from 'antd'; +import { useNavigate } from 'react-router'; +import { ArrowRightOutlined } from '@ant-design/icons'; + +export const WebsiteOverviewItem: React.FC<{ + websiteId: string; +}> = React.memo((props) => { + const workspaceId = useCurrentWorkspaceId(); + const navigate = useNavigate(); + + const { data: websiteInfo, isLoading } = trpc.website.info.useQuery({ + workspaceId, + websiteId: props.websiteId, + }); + + if (isLoading) { + return ; + } + + if (!websiteInfo) { + return ; + } + + return ( + + + + } + /> + ); +}); +WebsiteOverviewItem.displayName = 'WebsiteOverviewItem'; diff --git a/src/client/components/dashboard/items/index.tsx b/src/client/components/dashboard/items/index.tsx new file mode 100644 index 0000000..9714c6f --- /dev/null +++ b/src/client/components/dashboard/items/index.tsx @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; +import { DashboardItem, useDashboardStore } from '../../../store/dashboard'; +import { WebsiteOverviewItem } from './WebsiteOverviewItem'; +import { NotFoundTip } from '../../NotFoundTip'; +import { Button, Card } from 'antd'; +import React from 'react'; +import { DeleteOutlined } from '@ant-design/icons'; +import { useEvent } from '../../../hooks/useEvent'; + +interface DashboardGridItemProps { + item: DashboardItem; +} +export const DashboardGridItem: React.FC = React.memo( + (props) => { + const { isEditMode, removeItem } = useDashboardStore(); + const { key, id, title, type } = props.item; + + const inner = useMemo(() => { + if (type === 'websiteOverview') { + return ; + } else { + return ; + } + }, [id, type]); + + const handleDelete = useEvent( + (e: React.MouseEvent) => { + console.log('e', e, key); + e.stopPropagation(); + removeItem(key); + } + ); + + return ( + } + onClick={handleDelete} + /> + ) + } + > + {inner} + + ); + } +); +DashboardGridItem.displayName = 'DashboardGridItem'; diff --git a/src/client/components/website/WebsiteOverview.tsx b/src/client/components/website/WebsiteOverview.tsx index 79b95e1..b47ef1b 100644 --- a/src/client/components/website/WebsiteOverview.tsx +++ b/src/client/components/website/WebsiteOverview.tsx @@ -233,7 +233,7 @@ export const StatsChart: React.FC<{ formatter: (text) => formatDateWithUnit(text, props.unit), }, }, - } as ColumnConfig), + } satisfies ColumnConfig), [props.data, props.unit] ); diff --git a/src/client/pages/Dashboard.tsx b/src/client/pages/Dashboard.tsx index f917d23..723f563 100644 --- a/src/client/pages/Dashboard.tsx +++ b/src/client/pages/Dashboard.tsx @@ -1,153 +1,7 @@ -import React, { Fragment, useMemo, useState } from 'react'; -import { ArrowRightOutlined, EditOutlined } from '@ant-design/icons'; -import { Button, Divider, Empty } from 'antd'; -import { WebsiteOverview } from '../components/website/WebsiteOverview'; -import { useCurrentWorkspace } from '../store/user'; -import { Loading } from '../components/Loading'; -import { useWorspaceWebsites } from '../api/model/website'; -import { NoWorkspaceTip } from '../components/NoWorkspaceTip'; -import { useNavigate } from 'react-router'; -import { useEvent } from '../hooks/useEvent'; -import arrayMove from 'array-move'; -import SortableList, { SortableItem } from 'react-easy-sort'; -import { defaultErrorHandler, defaultSuccessHandler, trpc } from '../api/trpc'; -import { Link } from 'react-router-dom'; -import { DateFilter } from '../components/DateFilter'; +import React from 'react'; +import { Dashboard } from '../components/dashboard/Dashboard'; -export const Dashboard: React.FC = React.memo(() => { - const workspace = useCurrentWorkspace(); - const navigate = useNavigate(); - const [isEditLayout, setIsEditLayout] = useState(false); - const { isLoading, websiteList, handleSortEnd } = useDashboardWebsiteList(); - - if (!workspace) { - return ; - } - - if (isLoading) { - return ; - } - - return ( -
-
-
Dashboard
-
- {!isEditLayout && } - - {websiteList.length !== 0 && ( - - )} -
-
- - {isEditLayout ? ( - - {websiteList.map((website) => ( - -
- {website.name} -
-
- ))} -
- ) : ( -
- {websiteList.length === 0 && ( - -
There is no website has been created
- - - -
- } - /> - )} - - {websiteList.map((website, i) => ( - - {i !== 0 && } - - - - - } - /> - - ))} -
- )} - - ); +export const DashboardPage: React.FC = React.memo(() => { + return ; }); Dashboard.displayName = 'Dashboard'; - -function useDashboardWebsiteList() { - const workspace = useCurrentWorkspace(); - const workspaceId = workspace.id; - const { isLoading, websites } = useWorspaceWebsites(workspaceId); - const [dashboardOrder, setDashboardOrder] = useState( - workspace.dashboardOrder - ); - const updateDashboardOrderMutation = - trpc.workspace.updateDashboardOrder.useMutation({ - onSuccess: defaultSuccessHandler, - onError: defaultErrorHandler, - }); - - const websiteList = useMemo( - () => - websites.sort((a, b) => { - const aIndex = dashboardOrder.findIndex((item) => item === a.id); - const bIndex = dashboardOrder.findIndex((item) => item === b.id); - - // In both cases, if in the sorted list, they are sorted according to the sorted list. - // If not in the sorted list, put it first - return aIndex - bIndex; - }), - [websites, dashboardOrder] - ); - - const handleSortEnd = useEvent((oldIndex: number, newIndex: number) => { - const newOrder = arrayMove( - websiteList.map((w) => w.id), - oldIndex, - newIndex - ); - setDashboardOrder(newOrder); - - updateDashboardOrderMutation.mutate({ - workspaceId, - dashboardOrder: newOrder, - }); - }); - - return { - isLoading, - websiteList, - handleSortEnd, - }; -} diff --git a/src/client/store/dashboard.ts b/src/client/store/dashboard.ts new file mode 100644 index 0000000..5b46e15 --- /dev/null +++ b/src/client/store/dashboard.ts @@ -0,0 +1,83 @@ +import { create } from 'zustand'; +import { Layouts, Layout } from 'react-grid-layout'; +import { mapValues, without } from 'lodash'; +import { v1 as uuid } from 'uuid'; + +export type DashboardItemType = + | 'websiteOverview' + | 'websiteEvent' + | 'monitorHealthBar' + | 'monitorStatus' + | 'monitorChart' + | 'monitorEvent' + | 'serverStatus'; + +export interface DashboardItem { + key: string; // match with layout, not equal + id: string; + title: string; + type: DashboardItemType; +} + +interface DashboardState { + isEditMode: boolean; + switchEditMode: () => void; + layouts: Layouts; + items: DashboardItem[]; + addItem: (type: DashboardItemType, id: string, title: string) => void; + removeItem: (key: string) => void; +} + +export const defaultBlankLayouts = { + lg: [], +}; + +export const useDashboardStore = create((set, get) => ({ + isEditMode: false, + layouts: defaultBlankLayouts, + items: [], + switchEditMode: () => { + set({ isEditMode: !get().isEditMode }); + }, + addItem: (type: DashboardItemType, id: string, title: string) => { + const key = uuid(); + + set((state) => { + return { + layouts: mapValues(state.layouts, (layout) => [ + ...layout, + { ...defaultItemLayout[type], i: key }, + ]), + items: [ + ...state.items, + { + key, + id, + title, + type, + }, + ], + }; + }); + }, + removeItem: (key: string) => { + set((state) => { + return { + layouts: mapValues(state.layouts, (layout) => + layout.filter((l) => l.i !== key) + ), + items: state.items.filter((item) => item.key !== key), + }; + }); + }, +})); + +export const defaultItemLayout: Record> = { + websiteOverview: { x: Infinity, y: Infinity, w: 4, h: 12 }, + websiteEvent: { x: 0, y: 0, w: 4, h: 2 }, + monitorHealthBar: { x: 0, y: 0, w: 4, h: 2 }, + monitorStatus: { x: 0, y: 0, w: 4, h: 2 }, + monitorChart: { x: 0, y: 0, w: 4, h: 2 }, + monitorEvent: { x: 0, y: 0, w: 4, h: 2 }, + serverStatus: { x: 0, y: 0, w: 4, h: 2 }, +}; diff --git a/src/server/model/_schema/index.ts b/src/server/model/_schema/index.ts index 9cb8463..5444a31 100644 --- a/src/server/model/_schema/index.ts +++ b/src/server/model/_schema/index.ts @@ -10,10 +10,16 @@ export const jsonFieldSchema = z.union([ z.number(), ]); +export const workspaceDashboardLayoutSchema = z + .object({ + layouts: z.record(z.string(), z.array(z.any())), + items: z.array(z.any()), + }) + .nullable(); + export const workspaceSchema = z.object({ id: z.string(), name: z.string(), - dashboardOrder: z.array(z.string()), }); export const userInfoSchema = z.object({ @@ -23,7 +29,12 @@ export const userInfoSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), deletedAt: z.date().nullable(), - currentWorkspace: workspaceSchema.nullable(), + currentWorkspace: z.intersection( + workspaceSchema.nullable(), + z.object({ + dashboardLayout: workspaceDashboardLayoutSchema, + }) + ), workspaces: z.array( z.object({ role: z.string(), diff --git a/src/server/model/user.ts b/src/server/model/user.ts index e0d3561..938a137 100644 --- a/src/server/model/user.ts +++ b/src/server/model/user.ts @@ -3,6 +3,7 @@ import bcryptjs from 'bcryptjs'; import { ROLES, SYSTEM_ROLES } from '../utils/const'; import { jwtVerify } from '../middleware/auth'; import { TRPCError } from '@trpc/server'; +import { Prisma } from '@prisma/client'; async function hashPassword(password: string) { return await bcryptjs.hash(password, 10); @@ -30,6 +31,7 @@ const createUserSelect = { id: true, name: true, dashboardOrder: true, + dashboardLayout: true, }, }, workspaces: { @@ -39,12 +41,11 @@ const createUserSelect = { select: { id: true, name: true, - dashboardOrder: true, }, }, }, }, -}; +} satisfies Prisma.UserSelect; /** * Create User diff --git a/src/server/trpc/routers/website.ts b/src/server/trpc/routers/website.ts index 6c1ffe3..3379223 100644 --- a/src/server/trpc/routers/website.ts +++ b/src/server/trpc/routers/website.ts @@ -40,6 +40,27 @@ export const websiteRouter = router({ return count; }), + all: workspaceProcedure + .meta({ + openapi: { + method: 'GET', + path: `/workspace/{workspaceId}/website/all`, + tags: [OPENAPI_TAG.WEBSITE], + protect: true, + }, + }) + .output(z.array(websiteInfoSchema)) + .query(async ({ input }) => { + const { workspaceId } = input; + + const websites = await prisma.website.findMany({ + where: { + workspaceId, + }, + }); + + return websites; + }), info: workspaceProcedure .meta( buildWebsiteOpenapi({ diff --git a/src/server/trpc/routers/workspace.ts b/src/server/trpc/routers/workspace.ts index 4b91f4a..ad2740d 100644 --- a/src/server/trpc/routers/workspace.ts +++ b/src/server/trpc/routers/workspace.ts @@ -1,6 +1,7 @@ import { router, workspaceOwnerProcedure } from '../trpc'; import { z } from 'zod'; import { prisma } from '../../model/_client'; +import { workspaceDashboardLayoutSchema } from '../../model/_schema'; export const workspaceRouter = router({ updateDashboardOrder: workspaceOwnerProcedure @@ -21,4 +22,22 @@ export const workspaceRouter = router({ }, }); }), + saveDashboardLayout: workspaceOwnerProcedure + .input( + z.object({ + dashboardLayout: workspaceDashboardLayoutSchema, + }) + ) + .mutation(async ({ input }) => { + const { workspaceId, dashboardLayout } = input; + + await prisma.workspace.update({ + where: { + id: workspaceId, + }, + data: { + dashboardLayout, + }, + }); + }), });