Skip to content

Commit

Permalink
feat: connected surfaces table in ui
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Feb 2, 2025
1 parent 5be0e80 commit 5ddbbd1
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 36 deletions.
22 changes: 21 additions & 1 deletion openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,27 @@ paths:
schema:
$ref: '#/components/schemas/ErrorResponse'

/rescan:
/surfaces:
get:
summary: Get Connected surfaces
operationId: getConnectedSurfaces
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SurfaceInfo'
'400':
description: failed operation
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'

/surfaces/rescan:
post:
summary: Rescan for connected surfaces
operationId: postSurfaceesScan
Expand Down
1 change: 1 addition & 0 deletions satellite/src/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface SatelliteUiApi {
saveConfig: (newConfig: ApiConfigDataUpdate) => Promise<ApiConfigData>
getStatus: () => Promise<ApiStatusResponse>
rescanSurfaces: () => Promise<void>
connectedSurfaces: () => Promise<ApiSurfaceInfo[]>
}

export function compileStatus(client: CompanionSatelliteClient): ApiStatusResponse {
Expand Down
4 changes: 4 additions & 0 deletions satellite/src/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ApiConfigData,
ApiConfigDataUpdateElectron,
ApiStatusResponse,
ApiSurfaceInfo,
compileConfig,
compileStatus,
updateConfig,
Expand Down Expand Up @@ -264,6 +265,9 @@ ipcMain.handle('saveConfig', async (_e, newConfig: ApiConfigDataUpdateElectron):
updateConfig(appConfig, newConfig)
return compileConfig(appConfig)
})
ipcMain.handle('connectedSurfaces', async (): Promise<ApiSurfaceInfo[]> => {
return surfaceManager.getOpenSurfacesInfo()
})

// about window
ipcMain.handle('openShell', async (_e, url: string): Promise<void> => {
Expand Down
11 changes: 9 additions & 2 deletions satellite/src/electronPreload.cts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const { contextBridge, ipcRenderer } = require('electron')
// @ts-expect-error weird interop between cjs and mjs
import type { ApiConfigData, ApiConfigDataUpdateElectron, ApiStatusResponse, SatelliteUiApi } from './apiTypes.js'
import type {
ApiConfigData,
ApiConfigDataUpdateElectron,
ApiStatusResponse,
ApiSurfaceInfo,
SatelliteUiApi,
// @ts-expect-error weird interop between cjs and mjs
} from './apiTypes.js'

const electronApi: SatelliteUiApi = {
includeApiEnable: true,
Expand All @@ -10,6 +16,7 @@ const electronApi: SatelliteUiApi = {
getConfig: async (): Promise<ApiConfigData> => ipcRenderer.invoke('getConfig'),
saveConfig: async (newConfig: ApiConfigDataUpdateElectron): Promise<ApiConfigData> =>
ipcRenderer.invoke('saveConfig', newConfig),
connectedSurfaces: async (): Promise<ApiSurfaceInfo[]> => ipcRenderer.invoke('connectedSurfaces'),
}

contextBridge.exposeInMainWorld('electronApi', electronApi)
Expand Down
48 changes: 47 additions & 1 deletion satellite/src/generated/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,24 @@ export interface paths {
patch?: never;
trace?: never;
};
"/rescan": {
"/surfaces": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Connected surfaces */
get: operations["getConnectedSurfaces"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/surfaces/rescan": {
parameters: {
query?: never;
header?: never;
Expand Down Expand Up @@ -143,6 +160,35 @@ export interface operations {
};
};
};
getConnectedSurfaces: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description successful operation */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SurfaceInfo"][];
};
};
/** @description failed operation */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
postSurfaceesScan: {
parameters: {
query?: never;
Expand Down
4 changes: 2 additions & 2 deletions satellite/src/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,14 @@ export class RestServer {
}
})

this.router.post('/api/rescan', async (ctx) => {
this.router.post('/api/surfaces/rescan', async (ctx) => {
this.surfaceManager.scanForSurfaces()

ctx.body = 'OK'
})

this.router.get('/api/surfaces', async (ctx) => {
ctx.body = this.surfaceManager.getKnownSurfaces() satisfies ApiSurfaceInfo[]
ctx.body = this.surfaceManager.getOpenSurfacesInfo() satisfies ApiSurfaceInfo[]
})

this.app.use(this.router.routes()).use(this.router.allowedMethods())
Expand Down
2 changes: 1 addition & 1 deletion satellite/src/surface-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export class SurfaceManager {
})
}

public getKnownSurfaces(): ApiSurfaceInfo[] {
public getOpenSurfacesInfo(): ApiSurfaceInfo[] {
return Array.from(this.#surfaces.values()).map((surface) => ({
pluginId: surface.pluginId,
pluginName: this.#plugins.get(surface.pluginId)?.pluginName ?? 'Unknown',
Expand Down
9 changes: 7 additions & 2 deletions webui/src/Api/rest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SatelliteUiApi, ApiConfigData, ApiStatusResponse, ApiConfigDataUpdate } from './types'
import type { SatelliteUiApi, ApiConfigData, ApiStatusResponse, ApiConfigDataUpdate, ApiSurfaceInfo } from './types'
import type { paths } from '../../../satellite/src/generated/openapi'
import createClient from 'openapi-fetch'

Expand All @@ -24,7 +24,12 @@ export const SatelliteRestApi: SatelliteUiApi = {
return data
},
rescanSurfaces: async function (): Promise<void> {
const { data, error } = await client.POST('/rescan', {})
const { data, error } = await client.POST('/surfaces/rescan', {})
if (error) throw new Error(error.error)
return data
},
connectedSurfaces: async function (): Promise<ApiSurfaceInfo[]> {
const { data, error } = await client.GET('/surfaces', {})
if (error) throw new Error(error.error)
return data
},
Expand Down
52 changes: 52 additions & 0 deletions webui/src/app/ConnectedSurfacesTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { SurfacesRescan } from './SurfacesRescan'
import { useQuery } from '@tanstack/react-query'
import { useSatelliteApi } from '@/Api/Context'
import { ApiSurfaceInfo } from '@/Api/types'
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '@/components/ui/table'
import { BarLoader } from 'react-spinners'

export function ConnectedSurfacesTab(): JSX.Element {
const api = useSatelliteApi()
const connectedSurfaces = useQuery({
queryKey: ['connectedSurfaces'],
queryFn: async () => api.connectedSurfaces(),
})

return (
<div>
<h3 className="text-2xl font-bold dark:text-white">
Connected Surfaces
<SurfacesRescan className="ml-2 float-right" />
</h3>

{connectedSurfaces.isLoading ? <BarLoader color="#ffffff" className="mt-4" /> : null}
{connectedSurfaces.error ? <p>Error: {connectedSurfaces.error.message.toString()}</p> : null}
{connectedSurfaces.data && <ConnectedSurfacesList surfaces={connectedSurfaces.data} />}
</div>
)
}

function ConnectedSurfacesList({ surfaces }: { surfaces: ApiSurfaceInfo[] }): JSX.Element {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{surfaces.map((surface) => (
<TableRow key={surface.surfaceId}>
<TableCell>
<span className="font-medium">{surface.productName}</span>
<br />
<span className="font-medium text-gray-500">{surface.pluginName}</span>
</TableCell>
<TableCell className="font-medium">{surface.surfaceId}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
10 changes: 5 additions & 5 deletions webui/src/app/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { CardContent, CardHeader } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { ConnectionTab } from './ConnectionTab'
import { SurfacesTab } from './SurfacesTab'
import { ConnectedSurfacesTab } from './ConnectedSurfacesTab'

const queryClient = new QueryClient()

export function AppContent(): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<Tabs defaultValue="connection">
<CardHeader className="text-center">
<CardHeader className="text-center pb-3">
<TabsList>
<TabsTrigger value="connection">Connection</TabsTrigger>
<TabsTrigger value="surfaces">Surfaces</TabsTrigger>
<TabsTrigger value="connected-surfaces">Connected Surfaces</TabsTrigger>
</TabsList>
</CardHeader>
<CardContent>
<TabsContent value="connection">
<ConnectionTab />
</TabsContent>
<TabsContent value="surfaces">
<SurfacesTab />
<TabsContent value="connected-surfaces">
<ConnectedSurfacesTab />
</TabsContent>
</CardContent>
</Tabs>
Expand Down
4 changes: 2 additions & 2 deletions webui/src/app/SurfacesRescan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BeatLoader } from 'react-spinners'
import { useState } from 'react'
import { Button } from '@/components/ui/button'

export function SurfacesRescan(): JSX.Element {
export function SurfacesRescan({ className }: { className?: string }): JSX.Element {
const api = useSatelliteApi()

const [running, setRunning] = useState(false)
Expand All @@ -23,7 +23,7 @@ export function SurfacesRescan(): JSX.Element {

return (
<>
<Button onClick={doRescan} disabled={running}>
<Button onClick={doRescan} disabled={running} className={className}>
{running ? <BeatLoader /> : 'Rescan'}
</Button>
</>
Expand Down
20 changes: 0 additions & 20 deletions webui/src/app/SurfacesTab.tsx

This file was deleted.

76 changes: 76 additions & 0 deletions webui/src/components/ui/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as React from 'react'

import { cn } from '@/lib/utils'

const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
),
)
Table.displayName = 'Table'

const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />,
)
TableHeader.displayName = 'TableHeader'

const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
),
)
TableBody.displayName = 'TableBody'

const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} />
),
)
TableFooter.displayName = 'TableFooter'

const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
{...props}
/>
),
)
TableRow.displayName = 'TableRow'

const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
),
)
TableHead.displayName = 'TableHead'

const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
{...props}
/>
),
)
TableCell.displayName = 'TableCell'

const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
),
)
TableCaption.displayName = 'TableCaption'

export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }

0 comments on commit 5ddbbd1

Please sign in to comment.