Show active WebUI column in x:Application list and active app info card
This commit is contained in:
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user