Show active WebUI column in x:Application list and active app info card

This commit is contained in:
Steven RYDELL
2026-07-30 13:03:05 +02:00
parent 231f2ec3ee
commit aa48b90323
2 changed files with 79 additions and 5 deletions
+73 -4
View File
@@ -46,6 +46,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ObjectPicker } from '@/components/common/ObjectPicker'; import { ObjectPicker } from '@/components/common/ObjectPicker';
import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell'; import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
@@ -59,7 +60,7 @@ import { useAuthStore } from '@/stores/authStore';
import { useAccountStore } from '@/stores/accountStore'; import { useAccountStore } from '@/stores/accountStore';
import { useCacheStore } from '@/stores/cacheStore'; import { useCacheStore } from '@/stores/cacheStore';
import { resolveObject, resolveSchema, resolveList, getDisplayProperty } from '@/lib/schemaResolver'; import { resolveObject, resolveSchema, resolveList, getDisplayProperty } from '@/lib/schemaResolver';
import { jmapGetBatched, jmapQueryAll, jmapQueryAndGet, jmapSet, getAccountId } from '@/services/jmap/client'; import { jmapGetBatched, jmapQueryAll, jmapQueryAndGet, jmapQueryAllAndGet, jmapSet, getAccountId } from '@/services/jmap/client';
import type { Schema, Field, MassAction, ItemAction, Filter as FilterDef } from '@/types/schema'; import type { Schema, Field, MassAction, ItemAction, Filter as FilterDef } from '@/types/schema';
import type { JmapSetResponse, JmapSetError } from '@/types/jmap'; import type { JmapSetResponse, JmapSetError } from '@/types/jmap';
@@ -329,6 +330,12 @@ interface ConfirmAction {
onConfirm: () => void; onConfirm: () => void;
} }
function isActiveWebApplication(item: Record<string, unknown>): boolean {
const prefixes = item.urlPrefix;
if (!Array.isArray(prefixes)) return false;
return prefixes.includes('/admin') || prefixes.includes('/account');
}
interface DynamicListProps { interface DynamicListProps {
viewName: string; viewName: string;
} }
@@ -383,6 +390,36 @@ export function DynamicList({ viewName }: DynamicListProps) {
const [sort, setSort] = useState<SortState | null>(readUrlSort); const [sort, setSort] = useState<SortState | null>(readUrlSort);
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null); const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
const [activeWebApp, setActiveWebApp] = useState<Record<string, unknown> | null>(null);
const objectName = resolved?.obj.objectName;
const isWebApplications = objectName === 'x:Application';
useEffect(() => {
if (!isWebApplications || !schema) return;
let cancelled = false;
void (async () => {
try {
const accountId = getAccountId('x:Application');
const result = await jmapQueryAllAndGet(
'x:Application',
accountId,
{},
['id', 'description', 'enabled', 'urlPrefix'],
);
if (cancelled) return;
const active = result.list.find((item) => isActiveWebApplication(item));
setActiveWebApp(active ?? null);
} catch (err) {
console.error('Failed to fetch active web application:', err);
}
})();
return () => {
cancelled = true;
};
}, [isWebApplications, schema]);
useResetOnChange(viewName, () => { useResetOnChange(viewName, () => {
setItems([]); setItems([]);
@@ -400,7 +437,6 @@ export function DynamicList({ viewName }: DynamicListProps) {
setSort(readUrlSort()); setSort(readUrlSort());
}); });
const objectName = resolved?.obj.objectName;
const buildFilter = useCallback((): Record<string, unknown> => { const buildFilter = useCallback((): Record<string, unknown> => {
return buildJmapFilter({ return buildJmapFilter({
appliedFilters, appliedFilters,
@@ -1219,6 +1255,25 @@ export function DynamicList({ viewName }: DynamicListProps) {
</div> </div>
)} )}
{isWebApplications && activeWebApp && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">
{t('webApplications.activeWebUI', 'Active WebUI')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 pt-0">
<p className="text-sm text-muted-foreground">
{String(activeWebApp.description ?? '-')}
</p>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">{t('webApplications.version', 'Version')}:</span>
<Badge variant="secondary">{__APP_VERSION__}</Badge>
</div>
</CardContent>
</Card>
)}
<div className="rounded-lg border bg-background shadow-sm"> <div className="rounded-lg border bg-background shadow-sm">
{/* The scroll container must clip with the parent's inner radius {/* The scroll container must clip with the parent's inner radius
(outer radius minus the 1px border), otherwise filled header rows (outer radius minus the 1px border), otherwise filled header rows
@@ -1236,6 +1291,11 @@ export function DynamicList({ viewName }: DynamicListProps) {
/> />
</th> </th>
)} )}
{isWebApplications && (
<th className="w-16 px-3 py-3 text-left font-medium text-muted-foreground">
{t('list.active', 'Active')}
</th>
)}
{displayColumns.map((col) => ( {displayColumns.map((col) => (
<th key={col.name} className="px-3 py-3 text-left font-medium text-muted-foreground"> <th key={col.name} className="px-3 py-3 text-left font-medium text-muted-foreground">
<div className="flex items-center"> <div className="flex items-center">
@@ -1255,7 +1315,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
{loading && items.length === 0 ? ( {loading && items.length === 0 ? (
<tr> <tr>
<td <td
colSpan={displayColumns.length + (hasMassActions ? 1 : 0) + (hasItemActions ? 1 : 0)} colSpan={displayColumns.length + (hasMassActions ? 1 : 0) + (hasItemActions ? 1 : 0) + (isWebApplications ? 1 : 0)}
className="px-3 py-12 text-center" className="px-3 py-12 text-center"
> >
<Loader2 className="mx-auto h-6 w-6 animate-spin" /> <Loader2 className="mx-auto h-6 w-6 animate-spin" />
@@ -1264,7 +1324,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
) : items.length === 0 ? ( ) : items.length === 0 ? (
<tr> <tr>
<td <td
colSpan={displayColumns.length + (hasMassActions ? 1 : 0) + (hasItemActions ? 1 : 0)} colSpan={displayColumns.length + (hasMassActions ? 1 : 0) + (hasItemActions ? 1 : 0) + (isWebApplications ? 1 : 0)}
className="px-3 py-12 text-center text-muted-foreground" className="px-3 py-12 text-center text-muted-foreground"
> >
{t('list.noResults', 'No results found')} {t('list.noResults', 'No results found')}
@@ -1288,6 +1348,15 @@ export function DynamicList({ viewName }: DynamicListProps) {
/> />
</td> </td>
)} )}
{isWebApplications && (
<td className="px-3 py-2">
{activeWebApp?.id === item.id ? (
<Badge variant="default">{t('list.active', 'Active')}</Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</td>
)}
{displayColumns.map((col) => ( {displayColumns.map((col) => (
<td key={col.name} className="px-3 py-2"> <td key={col.name} className="px-3 py-2">
{renderCellValue( {renderCellValue(
+6 -1
View File
@@ -309,7 +309,8 @@
"showing": "Showing {{from}}-{{to}} of {{total}} {{name}}", "showing": "Showing {{from}}-{{to}} of {{total}} {{name}}",
"showingItems": "Showing {{count}} items", "showingItems": "Showing {{count}} items",
"sort": "Sort", "sort": "Sort",
"unknownError": "Unknown error" "unknownError": "Unknown error",
"active": "Active"
}, },
"login": { "login": {
"continue": "Continue", "continue": "Continue",
@@ -397,5 +398,9 @@
"failedToLoad": "Failed to load", "failedToLoad": "Failed to load",
"noGetResponse": "No get response", "noGetResponse": "No get response",
"objectNotFound": "Object not found" "objectNotFound": "Object not found"
},
"webApplications": {
"activeWebUI": "Active WebUI",
"version": "Version"
} }
} }