Add support for hosting forms and links in Panel sections
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index e77cbdc..f7a2982 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@jottocraft/iris-sdk",
- "version": "0.0.1-alpha.1",
+ "version": "0.0.1-alpha.2",
"homepage": "https://gitlab.com/jottocraft/iris",
"bugs": {
"url": "https://gitlab.com/jottocraft/iris/-/issues"
diff --git a/packages/sdk/src/conduit.ts b/packages/sdk/src/conduit.ts
index e223540..1232332 100644
--- a/packages/sdk/src/conduit.ts
+++ b/packages/sdk/src/conduit.ts
@@ -152,10 +152,17 @@
displayName: string;
icon?: string;
forms?: IrisPluginConduitFormRef<PluginType>[];
+ links?: IrisPluginConduitPanelLayoutLink[];
main?: IrisPluginConduitPanelLayoutField[];
sections?: IrisPluginConduitPanelLayoutSection<PluginType>[];
};
+export type IrisPluginConduitPanelLayoutLink = {
+ icon?: string;
+ label: string;
+ href: string;
+};
+
export type IrisPluginConduitPanelLayoutSection<PluginType extends IrisPluginConduit> = IrisPluginConduitPanelLayoutSectionProperties<PluginType> | IrisPluginConduitPanelLayoutSectionTable<PluginType> | IrisPluginConduitPanelLayoutSectionPanel<PluginType> | IrisPluginConduitPanelLayoutSectionForm<PluginType>;
export type IrisPluginConduitPanelLayoutSectionCommon = {
@@ -166,11 +173,15 @@
export type IrisPluginConduitPanelLayoutSectionProperties<PluginType extends IrisPluginConduit> = IrisPluginConduitPanelLayoutSectionCommon & {
type: "properties";
+ forms?: IrisPluginConduitFormRef<PluginType>[];
+ links?: IrisPluginConduitPanelLayoutLink[];
properties: IrisPluginConduitPanelLayoutField[];
};
export type IrisPluginConduitPanelLayoutSectionTable<PluginType extends IrisPluginConduit> = IrisPluginConduitPanelLayoutSectionCommon & {
type: "table";
+ forms?: IrisPluginConduitFormRef<PluginType>[];
+ links?: IrisPluginConduitPanelLayoutLink[];
columns: {
key: string;
displayName: string;
diff --git a/packages/webui/.gitignore b/packages/webui/.gitignore
index 7bd7f37..6ba266c 100644
--- a/packages/webui/.gitignore
+++ b/packages/webui/.gitignore
@@ -11,4 +11,5 @@
.wrangler
.dev.vars*
-src/IRIS_SITE.ts
\ No newline at end of file
+src/IRIS_SITE.ts
+src/IRIS_SITE.*.ts
\ No newline at end of file
diff --git a/packages/webui/src/app/components/Panel/FormsAndLinks.tsx b/packages/webui/src/app/components/Panel/FormsAndLinks.tsx
new file mode 100644
index 0000000..a302a48
--- /dev/null
+++ b/packages/webui/src/app/components/Panel/FormsAndLinks.tsx
@@ -0,0 +1,52 @@
+// Iris
+// Copyright (c) jottocraft 2025
+
+import { Button, Link } from "@ffxp/react-dom";
+import type { IrisPluginConduit, IrisPluginConduitFormRef, IrisPluginConduitPanelLayoutLink } from "@jottocraft/iris-sdk";
+import { useEffect, useState } from "react";
+
+import { usePanelNavigation } from "~/app/data/panels";
+import { usePlugins } from "~/app/data/root";
+
+export default function FormsAndLinks({ host, layout, small }: {
+ host: IrisWebuiPanelHost;
+ layout: {
+ forms?: IrisPluginConduitFormRef<IrisPluginConduit>[];
+ links?: IrisPluginConduitPanelLayoutLink[];
+ };
+ small?: boolean;
+}) {
+ const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
+ const plugins = usePlugins()!;
+
+ const plugin = plugins[panelNavigation.pluginId];
+
+ return (
+ <div className="space-y-2">
+ {layout.forms?.length ? (
+ <div>
+ {layout.forms.map((ref) => {
+ const form = plugin.plugin.forms[ref.id];
+
+ return (
+ <div className="inline-block m-1">
+ <Button size={small ? "small" : "default"} onClick={() => host.setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
+ </div>
+ );
+ })}
+ </div>
+ ) : null}
+ {layout.links?.length ? (
+ <div>
+ {layout.links.map((link) => {
+ return (
+ <div className="inline-block m-1">
+ <Link leadingIcon={link.icon} trailingIcon="open_in_new" href={link.href} inNew>{link.label}</Link>
+ </div>
+ );
+ })}
+ </div>
+ ) : null}
+ </div>
+ );
+}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/Panel/index.tsx b/packages/webui/src/app/components/Panel/index.tsx
index 4a847d0..09bef0d 100644
--- a/packages/webui/src/app/components/Panel/index.tsx
+++ b/packages/webui/src/app/components/Panel/index.tsx
@@ -7,17 +7,16 @@
import { useSearchParams } from "react-router";
import { YggdrasilStatus } from "@jottocraft/yggdrasil";
-import { usePlugins } from "~/app/data/root";
import PanelSection from "~/app/components/PanelSection";
import { usePanelNavigation } from "~/app/data/panels";
import Form from "~/app/components/Form";
import Loading from "~/app/components/Loading";
import { useError } from "~/app/data/error";
import ErrorScreen from "~/app/components/ErrorScreen";
+import FormsAndLinks from "~/app/components/Panel/FormsAndLinks";
export default function Panel() {
const panelNavigation = usePanelNavigation()!;
- const plugins = usePlugins()!;
const [activeForm, setActiveForm] = useState<IrisWebuiFormRef | null>(null);
const [selectedSection, setSelectedSection] = useState<string | null>(null);
const error = useError();
@@ -45,10 +44,11 @@
);
}
- const plugin = plugins[panelNavigation.pluginId];
const layout = panelNavigation.layout;
const activeSection = layout.sections?.length === 1 ? layout.sections[0] : (layout.sections?.find(section => (section.id === selectedSection)) ?? null);
+ const host = { setActiveForm };
+
return (
<>
{activeForm ? (
@@ -64,17 +64,9 @@
<h1>{layout.displayName}</h1>
</div>
) : null}
- {layout.forms ? (
+ {layout.forms?.length || layout.links?.length ? (
<div>
- {layout.forms?.map((ref) => {
- const form = plugin.plugin.forms[ref.id];
-
- return (
- <div className="inline-block m-1">
- <Button size={panelNavigation.root ? "default" : "small"} onClick={() => setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
- </div>
- );
- })}
+ <FormsAndLinks host={host} layout={layout} small={!panelNavigation.root} />
</div>
) : null}
{layout.main ? (
@@ -107,7 +99,7 @@
}))} />
) : null}
{activeSection ? (
- <PanelSection section={activeSection} />
+ <PanelSection host={host} section={activeSection} />
) : null}
</div>
) : null}
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx
index c851c5b..efa87b4 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionForm.tsx
@@ -6,7 +6,7 @@
import Form from "~/app/components/Form";
import { usePanelNavigation } from "~/app/data/panels";
-export default function PanelSectionForm({ section }: { section: IrisPluginConduitPanelLayoutSectionForm<IrisPluginConduit>; }) {
+export default function PanelSectionForm({ section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionForm<IrisPluginConduit>; }) {
const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
return (
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx
index a998bdd..4e4e497 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionPanel.tsx
@@ -9,7 +9,7 @@
import Panel from "~/app/components/Panel";
import PanelBreadcrumbs from "~/app/components/PanelBreadcrumbs";
-export default function PanelSectionPanel({ section }: { section: IrisPluginConduitPanelLayoutSectionPanel<IrisPluginConduit>; }) {
+export default function PanelSectionPanel({ section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionPanel<IrisPluginConduit>; }) {
const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
useEffect(() => {
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
index 4f05e9c..7fbb691 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionProperties.tsx
@@ -4,26 +4,35 @@
import { Icon } from "@ffxp/react-dom";
import type { IrisPluginConduit, IrisPluginConduitPanelLayoutSectionProperties } from "@jottocraft/iris-sdk";
-export default function PanelSectionProperties({ section }: { section: IrisPluginConduitPanelLayoutSectionProperties<IrisPluginConduit>; }) {
+import FormsAndLinks from "~/app/components/Panel/FormsAndLinks";
+
+export default function PanelSectionProperties({ host, section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionProperties<IrisPluginConduit>; }) {
return (
- <div className="w-full overflow-auto">
- <table>
- <tbody>
- {section.properties.map(property => (
- <tr>
- <td className="p-2">
- <div className="flex flex-row flex-shrink-0 items-center space-x-1 text-type-10 font-medium">
- {property.icon ? <Icon>{property.icon}</Icon> : null}
- <div className="whitespace-nowrap">{property.displayName}</div>
- </div>
- </td>
- <td className="p-2">
- <div className="text-sm text-type-20">{property.value}</div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
+ <div className="space-y-4">
+ {section.forms?.length || section.links?.length ? (
+ <div>
+ <FormsAndLinks host={host} layout={section} small />
+ </div>
+ ) : null}
+ <div className="w-full overflow-auto">
+ <table>
+ <tbody>
+ {section.properties.map(property => (
+ <tr>
+ <td className="p-2">
+ <div className="flex flex-row flex-shrink-0 items-center space-x-1 text-type-10 font-medium">
+ {property.icon ? <Icon>{property.icon}</Icon> : null}
+ <div className="whitespace-nowrap">{property.displayName}</div>
+ </div>
+ </td>
+ <td className="p-2">
+ <div className="text-sm text-type-20">{property.value}</div>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
</div>
);
}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx b/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
index 50c40d1..0ba30c9 100644
--- a/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
+++ b/packages/webui/src/app/components/PanelSection/PanelSectionTable.tsx
@@ -8,16 +8,11 @@
import { panelRefHref, usePanelNavigation } from "~/app/data/panels";
import { usePlugins } from "~/app/data/root";
-import Form from "~/app/components/Form";
+import FormsAndLinks from "~/app/components/Panel/FormsAndLinks";
-export default function PanelSectionTable({ section }: { section: IrisPluginConduitPanelLayoutSectionTable<IrisPluginConduit>; }) {
+export default function PanelSectionTable({ host, section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSectionTable<IrisPluginConduit>; }) {
const panelNavigation = usePanelNavigation() as IrisWebuiPanelNavigationCtxActive;
const plugins = usePlugins()!;
- const [activeForm, setActiveForm] = useState<IrisWebuiFormRef | null>(null);
-
- useEffect(() => {
- setActiveForm(null);
- }, [section]);
const plugin = plugins[panelNavigation.pluginId];
@@ -44,122 +39,119 @@
}
return (
- <>
- {activeForm ? (
- <Form ref={activeForm} onFinished={() => {
- setActiveForm(null);
- panelNavigation.refreshLayout();
- }} />
+ <div className="space-y-4">
+ {section.forms?.length || section.links?.length ? (
+ <div>
+ <FormsAndLinks host={host} layout={section} small />
+ </div>
) : null}
- <div className="space-y-4">
- {section.canSearch ? (
- <div className="flex flex-row items-center justify-end">
- <TextInput defaultValue={panelNavigation.activeState.sections[section.id]?.table?.search ?? ""} onKeyDown={e => {
- const search = e.currentTarget.value || undefined;
- if (e.key === "Enter") {
+ {section.canSearch ? (
+ <div className="flex flex-row items-center justify-end">
+ <TextInput defaultValue={panelNavigation.activeState.sections[section.id]?.table?.search ?? ""} onKeyDown={e => {
+ const search = e.currentTarget.value || undefined;
+ if (e.key === "Enter") {
+ panelNavigation.patchActiveSectionState(section.id, {
+ table: {
+ ...panelNavigation.activeState.sections[section.id]?.table,
+ search,
+ offset: 0
+ }
+ });
+ }
+ }} placeholder="Search" icon="search" />
+ </div>
+ ) : null}
+ <div className="w-full overflow-auto">
+ <table className="w-full">
+ <thead>
+ <tr className="border-b border-ui-30 text-left">
+ {section.columns.map(column => (
+ <th className="p-2 whitespace-nowrap font-medium">
+ <div className="flex flex-row items-center flex-nowrap space-x-1">
+ {column.icon ? <Icon>{column.icon}</Icon> : null}
+ <div>{column.displayName}</div>
+ </div>
+ </th>
+ ))}
+ </tr>
+ </thead>
+ <tbody>
+ {section.rows.map((row, i) => (
+ <tr className={i % 2 === 0 ? undefined : "bg-ui-5"}>
+ {section.columns.map(column => {
+ const link = row.links?.[column.key];
+ return (
+ <td className="p-2 whitespace-nowrap text-sm text-type-20">
+ {link ? (
+ <RouterLink to={panelRefHref(panelNavigation.pluginId, link)} onClick={(e) => panelNavigation.navigate(link, e)}>
+ <Link>{row.vals[column.key] ?? null}</Link>
+ </RouterLink>
+ ) : (
+ row.vals[column.key] ?? null
+ )}
+ </td>
+ );
+ })}
+ {row.forms ? (
+ <td>
+ {row.forms.map(ref => {
+ const form = plugin.plugin.forms[ref.id];
+
+ return (
+ <div className="inline-block m-1">
+ <Button size="small" onClick={() => host.setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
+ </div>
+ );
+ })}
+ </td>
+ ) : null}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ {pagination ? (
+ <div className="flex flex-row items-center justify-between">
+ <div className="text-sm text-type-30">Showing rows {pagination.firstRow + 1}-{pagination.firstRow + section.rows.length} of {pagination.totalRows}</div>
+ <div className="flex flex-row items-center">
+ {(pagination.visiblePages[0] !== 1) ? (
+ <>
+ <Icon className="text-type-60">more_horiz</Icon>
+ <div className="px-1" />
+ </>
+ ) : null}
+ {pagination.visiblePages.map(page => (
+ <Button onClick={() => {
panelNavigation.patchActiveSectionState(section.id, {
table: {
...panelNavigation.activeState.sections[section.id]?.table,
- search,
- offset: 0
+ offset: (page - 1) * pagination.rowsPerPage
}
});
+ }} variant={page === pagination.activePage ? "primary" : "borderless"} size="small">{String(page)}</Button>
+ ))}
+ {(pagination.visiblePages[pagination.visiblePages.length - 1] !== pagination.totalPages) ? (
+ <>
+ <div className="px-1" />
+ <Icon className="text-type-60">more_horiz</Icon>
+ </>
+ ) : null}
+ <div className="px-4" />
+ <TextInput onKeyDown={e => {
+ const val = Number(e.currentTarget.value);
+ if ((e.key === "Enter") && !isNaN(val) && (val >= 1) && (val <= pagination.totalPages)) {
+ panelNavigation.patchActiveSectionState(section.id, {
+ table: {
+ ...panelNavigation.activeState.sections[section.id]?.table,
+ offset: (val - 1) * pagination.rowsPerPage
+ }
+ });
+ e.currentTarget.value = "";
}
- }} placeholder="Search" icon="search" />
+ }} type="number" placeholder="Jump to" width={100} />
</div>
- ) : null}
- <div className="w-full overflow-auto">
- <table className="w-full">
- <thead>
- <tr className="border-b border-ui-30 text-left">
- {section.columns.map(column => (
- <th className="p-2 whitespace-nowrap font-medium">
- <div className="flex flex-row items-center flex-nowrap space-x-1">
- {column.icon ? <Icon>{column.icon}</Icon> : null}
- <div>{column.displayName}</div>
- </div>
- </th>
- ))}
- </tr>
- </thead>
- <tbody>
- {section.rows.map((row, i) => (
- <tr className={i % 2 === 0 ? undefined : "bg-ui-5"}>
- {section.columns.map(column => {
- const link = row.links?.[column.key];
- return (
- <td className="p-2 whitespace-nowrap text-sm text-type-20">
- {link ? (
- <RouterLink to={panelRefHref(panelNavigation.pluginId, link)} onClick={(e) => panelNavigation.navigate(link, e)}>
- <Link>{row.vals[column.key] ?? null}</Link>
- </RouterLink>
- ) : (
- row.vals[column.key] ?? null
- )}
- </td>
- );
- })}
- {row.forms ? (
- <td>
- {row.forms.map(ref => {
- const form = plugin.plugin.forms[ref.id];
-
- return (
- <div className="inline-block m-1">
- <Button size="small" onClick={() => setActiveForm(ref)} iconLeading={form.icon}>{form.displayName}</Button>
- </div>
- );
- })}
- </td>
- ) : null}
- </tr>
- ))}
- </tbody>
- </table>
</div>
- {pagination ? (
- <div className="flex flex-row items-center justify-between">
- <div className="text-sm text-type-30">Showing rows {pagination.firstRow + 1}-{pagination.firstRow + section.rows.length} of {pagination.totalRows}</div>
- <div className="flex flex-row items-center">
- {(pagination.visiblePages[0] !== 1) ? (
- <>
- <Icon className="text-type-60">more_horiz</Icon>
- <div className="px-1" />
- </>
- ) : null}
- {pagination.visiblePages.map(page => (
- <Button onClick={() => {
- panelNavigation.patchActiveSectionState(section.id, {
- table: {
- ...panelNavigation.activeState.sections[section.id]?.table,
- offset: (page - 1) * pagination.rowsPerPage
- }
- });
- }} variant={page === pagination.activePage ? "primary" : "borderless"} size="small">{String(page)}</Button>
- ))}
- {(pagination.visiblePages[pagination.visiblePages.length - 1] !== pagination.totalPages) ? (
- <>
- <div className="px-1" />
- <Icon className="text-type-60">more_horiz</Icon>
- </>
- ) : null}
- <div className="px-4" />
- <TextInput onKeyDown={e => {
- const val = Number(e.currentTarget.value);
- if ((e.key === "Enter") && !isNaN(val) && (val >= 1) && (val <= pagination.totalPages)) {
- panelNavigation.patchActiveSectionState(section.id, {
- table: {
- ...panelNavigation.activeState.sections[section.id]?.table,
- offset: (val - 1) * pagination.rowsPerPage
- }
- });
- e.currentTarget.value = "";
- }
- }} type="number" placeholder="Jump to" width={100} />
- </div>
- </div>
- ) : null}
- </div>
- </>
+ ) : null}
+ </div>
);
}
\ No newline at end of file
diff --git a/packages/webui/src/app/components/PanelSection/index.tsx b/packages/webui/src/app/components/PanelSection/index.tsx
index f79aaae..746e68d 100644
--- a/packages/webui/src/app/components/PanelSection/index.tsx
+++ b/packages/webui/src/app/components/PanelSection/index.tsx
@@ -8,22 +8,22 @@
import PanelSectionPanel from "~/app/components/PanelSection/PanelSectionPanel";
import PanelSectionForm from "~/app/components/PanelSection/PanelSectionForm";
-export default function PanelSection({ section }: { section: IrisPluginConduitPanelLayoutSection<IrisPluginConduit>; }) {
+export default function PanelSection({ host, section }: { host: IrisWebuiPanelHost; section: IrisPluginConduitPanelLayoutSection<IrisPluginConduit>; }) {
if (section.type === "properties") {
return (
- <PanelSectionProperties section={section} />
+ <PanelSectionProperties host={host} section={section} />
);
} else if (section.type === "table") {
return (
- <PanelSectionTable section={section} />
+ <PanelSectionTable host={host} section={section} />
);
} else if (section.type === "panel") {
return (
- <PanelSectionPanel section={section} />
+ <PanelSectionPanel host={host} section={section} />
);
- } else if (section.type === "form") {
+ } else if (section.type === "form") {
return (
- <PanelSectionForm section={section} />
+ <PanelSectionForm host={host} section={section} />
);
} else {
throw new Error("Unrecognized panel layout section type " + (section as any).type);
diff --git a/packages/webui/src/app/components/Shell.tsx b/packages/webui/src/app/components/Shell.tsx
index 99c8118..45e6f0f 100644
--- a/packages/webui/src/app/components/Shell.tsx
+++ b/packages/webui/src/app/components/Shell.tsx
@@ -39,7 +39,7 @@
window.location.reload();
}
}}>
- Signed in as {auth?.claims.email as string}
+ {"Signed in as " + auth?.claims.email as string}
</Button>
</>
)}>
diff --git a/packages/webui/src/app/root.tsx b/packages/webui/src/app/root.tsx
index 55ea1d6..a131528 100644
--- a/packages/webui/src/app/root.tsx
+++ b/packages/webui/src/app/root.tsx
@@ -60,7 +60,7 @@
authUrl.searchParams.set("client_id", IRIS_SITE.auth.clientId);
authUrl.searchParams.set("redirect_uri", redirectUrl.href);
authUrl.searchParams.set("response_type", "id_token");
- authUrl.searchParams.set("scope", IRIS_SITE.auth.scope ?? "openid email profile");
+ authUrl.searchParams.set("scope", "openid email profile");
authUrl.searchParams.set("nonce", nonce);
window.location.href = authUrl.href;
}
diff --git a/packages/webui/src/webui.d.ts b/packages/webui/src/webui.d.ts
index d17ec39..1188de6 100644
--- a/packages/webui/src/webui.d.ts
+++ b/packages/webui/src/webui.d.ts
@@ -43,4 +43,8 @@
displayName: string;
icon?: string;
ref: IrisWebuiPanelRef;
+};
+
+declare type IrisWebuiPanelHost = {
+ setActiveForm: (ref: IrisWebuiFormRef) => void;
};
\ No newline at end of file