Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions apps/console/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import { useState, useEffect, lazy, Suspense, useMemo, type ReactNode } from 'react';
import { ObjectForm } from '@object-ui/plugin-form';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Empty, EmptyTitle } from '@object-ui/components';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
import { toast } from 'sonner';
import { SchemaRendererProvider } from '@object-ui/react';
import type { ConnectionState } from './dataSource';
Expand Down Expand Up @@ -368,15 +368,24 @@ function findFirstRoute(items: any[]): string {

// Redirect root to default app
function RootRedirect() {
const { apps, loading } = useMetadata();
const { apps, loading, error } = useMetadata();
const activeApps = apps.filter((a: any) => a.active !== false);
const defaultApp = activeApps.find((a: any) => a.isDefault === true) || activeApps[0];

if (loading) return <LoadingScreen />;
if (defaultApp) {
return <Navigate to={`/apps/${defaultApp.name}`} replace />;
}
return <LoadingScreen />;
return (
<div className="h-screen flex items-center justify-center">
<Empty>
<EmptyTitle>{error ? 'Failed to Load Configuration' : 'No Apps Configured'}</EmptyTitle>
<EmptyDescription>
{error ? error.message : 'No applications have been registered. Check your ObjectStack configuration.'}
</EmptyDescription>
</Empty>
</div>
Comment on lines +381 to +387
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Empty state implementation is missing a visual icon/media element that is consistently used across other Empty states in the codebase. For consistency and better UX, consider adding an icon similar to other error/empty states:

<Empty>
  <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
    <AlertCircle className="h-6 w-6 text-muted-foreground" />
  </div>
  <EmptyTitle>{error ? 'Failed to Load Configuration' : 'No Apps Configured'}</EmptyTitle>
  <EmptyDescription>
    {error ? error.message : 'No applications have been registered. Check your ObjectStack configuration.'}
  </EmptyDescription>
</Empty>

You'll need to import AlertCircle or a similar icon from lucide-react. See DashboardView.tsx:39-48, ErrorBoundary.tsx:30-38, or PageView.tsx:25-34 for examples of this pattern.

Copilot uses AI. Check for mistakes.
);
}

export function App() {
Expand Down
86 changes: 18 additions & 68 deletions apps/console/src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,98 +48,48 @@ export function createHandlers(baseUrl: string, kernel: ObjectKernel, driver: In
return HttpResponse.json(response, { status: 200 });
}),

// ── Metadata: list objects ───────────────────────────────────────────
http.get(`${prefix}${baseUrl}/meta/objects`, async () => {
const response = await protocol.getMetaItems({ type: 'object' });
// ── Metadata: list items by type ────────────────────────────────────
// The client sends GET /meta/<type> where <type> is singular (e.g. app,
// object, dashboard, report, page). We also keep the legacy plural
// routes (/meta/apps, /meta/objects, …) for backward compatibility.
// A single dynamic handler covers both forms.
http.get(`${prefix}${baseUrl}/meta/:type`, async ({ params }) => {
const metadataType = params.type as string;
const response = await protocol.getMetaItems({ type: metadataType });
return HttpResponse.json(response, { status: 200 });
}),
http.get(`${prefix}${baseUrl}/metadata/objects`, async () => {
const response = await protocol.getMetaItems({ type: 'object' });
http.get(`${prefix}${baseUrl}/metadata/:type`, async ({ params }) => {
const metadataType = params.type as string;
const response = await protocol.getMetaItems({ type: metadataType });
return HttpResponse.json(response, { status: 200 });
}),

// ── Metadata: single object (legacy /meta/objects/:name) ─────────────
http.get(`${prefix}${baseUrl}/meta/objects/:objectName`, async ({ params }) => {
// ── Metadata: single item by type + name ─────────────────────────────
http.get(`${prefix}${baseUrl}/meta/:type/:name`, async ({ params }) => {
try {
const response = await protocol.getMetaItem({
type: 'object',
name: params.objectName as string
});
return HttpResponse.json(response || { error: 'Not found' }, { status: response ? 200 : 404 });
} catch (e) {
return HttpResponse.json({ error: String(e) }, { status: 500 });
}
}),

// ── Metadata: single object (/meta/object/:name & /metadata/object/:name)
http.get(`${prefix}${baseUrl}/meta/object/:objectName`, async ({ params }) => {
try {
const response = await protocol.getMetaItem({
type: 'object',
name: params.objectName as string
type: params.type as string,
name: params.name as string
});
const payload = (response && response.item) ? response.item : response;
return HttpResponse.json(payload || { error: 'Not found' }, { status: payload ? 200 : 404 });
} catch (e) {
console.error('[MSW] error getting meta item', e);
return HttpResponse.json({ error: String(e) }, { status: 500 });
}
Comment on lines 76 to 78
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The console.error logging was removed from the error handling block. This will make debugging MSW handler failures more difficult. Consider keeping the error logging while still returning the error response, for example:

} catch (e) {
  console.error('[MSW] error getting meta item', e);
  return HttpResponse.json({ error: String(e) }, { status: 500 });
}

This applies to both /meta/:type/:name and /metadata/:type/:name handlers.

Copilot uses AI. Check for mistakes.
}),

http.get(`${prefix}${baseUrl}/metadata/object/:objectName`, async ({ params }) => {
http.get(`${prefix}${baseUrl}/metadata/:type/:name`, async ({ params }) => {
try {
const response = await protocol.getMetaItem({
type: 'object',
name: params.objectName as string
type: params.type as string,
name: params.name as string
});
const payload = (response && response.item) ? response.item : response;
return HttpResponse.json(payload || { error: 'Not found' }, { status: payload ? 200 : 404 });
} catch (e) {
console.error('[MSW] error getting meta item', e);
return HttpResponse.json({ error: String(e) }, { status: 500 });
}
}),
Comment on lines +56 to 91
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity and to follow MSW best practices, consider reordering the metadata handlers so that more specific routes (with more path segments) are defined before less specific routes. While MSW's path-to-regexp should handle this correctly based on specificity, explicit ordering makes the code more maintainable and less prone to subtle bugs:

  1. /meta/:type/:name (lines 68-79)
  2. /metadata/:type/:name (lines 80-91)
  3. /meta/:type (lines 56-60)
  4. /metadata/:type (lines 61-65)

This ensures that requests like /meta/object/User will definitely match the :type/:name handler rather than relying on MSW's internal matching algorithm.

Copilot uses AI. Check for mistakes.

// ── Metadata: apps ──────────────────────────────────────────────────
http.get(`${prefix}${baseUrl}/meta/apps`, async () => {
const response = await protocol.getMetaItems({ type: 'app' });
return HttpResponse.json(response, { status: 200 });
}),
http.get(`${prefix}${baseUrl}/metadata/apps`, async () => {
const response = await protocol.getMetaItems({ type: 'app' });
return HttpResponse.json(response, { status: 200 });
}),

// ── Metadata: dashboards ────────────────────────────────────────────
http.get(`${prefix}${baseUrl}/meta/dashboards`, async () => {
const response = await protocol.getMetaItems({ type: 'dashboard' });
return HttpResponse.json(response, { status: 200 });
}),
http.get(`${prefix}${baseUrl}/metadata/dashboards`, async () => {
const response = await protocol.getMetaItems({ type: 'dashboard' });
return HttpResponse.json(response, { status: 200 });
}),

// ── Metadata: reports ───────────────────────────────────────────────
http.get(`${prefix}${baseUrl}/meta/reports`, async () => {
const response = await protocol.getMetaItems({ type: 'report' });
return HttpResponse.json(response, { status: 200 });
}),
http.get(`${prefix}${baseUrl}/metadata/reports`, async () => {
const response = await protocol.getMetaItems({ type: 'report' });
return HttpResponse.json(response, { status: 200 });
}),

// ── Metadata: pages ─────────────────────────────────────────────────
http.get(`${prefix}${baseUrl}/meta/pages`, async () => {
const response = await protocol.getMetaItems({ type: 'page' });
return HttpResponse.json(response, { status: 200 });
}),
http.get(`${prefix}${baseUrl}/metadata/pages`, async () => {
const response = await protocol.getMetaItems({ type: 'page' });
return HttpResponse.json(response, { status: 200 });
}),

// ── Data: find all ──────────────────────────────────────────────────
http.get(`${prefix}${baseUrl}/data/:objectName`, async ({ params, request }) => {
const url = new URL(request.url);
Expand Down