39948-vm/frontend/src/pages/ui-elements.tsx
2026-03-17 12:06:17 +00:00

162 lines
5.6 KiB
TypeScript

import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import { useRouter } from 'next/router'
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import axios from 'axios'
import CardBox from '../components/CardBox'
import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import { getPageTitle } from '../config'
import ElementPreview from '../components/UiElements/ElementPreview'
import {
getDefaultSettings,
normalizeUiElement,
toElementLabel,
UI_ELEMENT_TYPES,
} from '../components/UiElements/defaults'
import { UiElementItem, UiElementRow } from '../components/UiElements/types'
const UiElementsPage = () => {
const router = useRouter()
const [elements, setElements] = useState<UiElementItem[]>([])
const [isLoading, setIsLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const fetchRows = useCallback(async () => {
const response = await axios.get('/ui-elements?limit=1000&page=0&sort=asc&field=sort_order')
return Array.isArray(response?.data?.rows) ? response.data.rows : []
}, [])
const bootstrapMissingElements = useCallback(async (rows: UiElementRow[]) => {
const existingTypes = new Set(rows.map((row) => row.element_type))
const missingTypes = UI_ELEMENT_TYPES.filter((type) => !existingTypes.has(type))
const safeStartOrder = Math.max(
1,
...rows
.map((row) => Number(row.sort_order))
.filter((value) => Number.isFinite(value))
.map((value) => Math.trunc(value) + 1),
)
if (!missingTypes.length) return
const createResults = await Promise.allSettled(
missingTypes.map((elementType, index) =>
axios.post('/ui-elements', {
data: {
element_type: elementType,
name: toElementLabel(elementType),
sort_order: safeStartOrder + index,
settings_json: JSON.stringify(getDefaultSettings(elementType)),
},
}),
),
)
const rejected = createResults.filter((result) => result.status === 'rejected') as PromiseRejectedResult[]
if (rejected.length) {
const reasons = rejected
.map((result) => {
const err = result.reason
return err?.response?.data?.message || err?.message || 'Unknown create error'
})
.join('; ')
throw new Error(`Failed to bootstrap some UI elements: ${reasons}`)
}
}, [])
const loadElements = useCallback(async () => {
setIsLoading(true)
setErrorMessage('')
try {
const initialRows = (await fetchRows()) as UiElementRow[]
await bootstrapMissingElements(initialRows)
const rowsAfterBootstrap = (await fetchRows()) as UiElementRow[]
const normalized = rowsAfterBootstrap
.map((row) => normalizeUiElement(row))
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
setElements(normalized)
} catch (error: any) {
const message = error?.response?.data?.message || error?.message || 'Failed to load UI elements.'
setErrorMessage(message)
setElements([])
console.error('Failed to load UI elements:', error)
} finally {
setIsLoading(false)
}
}, [bootstrapMissingElements, fetchRows])
useEffect(() => {
loadElements()
}, [loadElements])
const sortedElements = useMemo(
() => [...elements].sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)),
[elements],
)
return (
<>
<Head>
<title>{getPageTitle('UI Elements')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='UI Elements' main>
{''}
</SectionTitleLineWithButton>
{errorMessage ? <CardBox className='mb-4 text-sm text-red-600'>{errorMessage}</CardBox> : null}
<CardBox>
{isLoading ? (
<p className='text-sm text-gray-500'>Loading platform elements...</p>
) : !sortedElements.length ? (
<p className='text-sm text-gray-500'>No UI elements found.</p>
) : (
<div className='overflow-x-auto'>
<table className='w-full min-w-[680px] text-sm'>
<thead>
<tr className='border-b border-gray-200 text-left dark:border-dark-700'>
<th className='py-2 pr-3 font-semibold'>Name</th>
<th className='py-2 pr-3 font-semibold'>Type</th>
<th className='py-2 pr-3 font-semibold'>Preview</th>
</tr>
</thead>
<tbody>
{sortedElements.map((item) => (
<tr
key={item.id}
className='cursor-pointer border-b border-gray-100 hover:bg-gray-50 dark:border-dark-800 dark:hover:bg-dark-800'
onClick={() => router.push(`/ui-elements/${item.id}`)}
>
<td className='py-3 pr-3 font-semibold'>{item.name}</td>
<td className='py-3 pr-3 text-xs text-gray-500'>{toElementLabel(item.elementType)}</td>
<td className='py-3 pr-3'>
<ElementPreview item={item} />
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardBox>
</SectionMain>
</>
)
}
UiElementsPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default UiElementsPage