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
9 changes: 8 additions & 1 deletion plugins/example-plugin/web/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Runtime contract: `../../../web/packages/studio/src/plugins/types.ts`.
| Deps | externalize the shared set in `vite.config.ts`; bundle the rest | bundle react / react-dom / react-router / foundations |

Studio injects everything a plugin needs through a **single `host` prop**
(`host.workspaceId`, `host.auth`, `host.sdk`, `host.navigation`,
(`host.workspaceId`, `host.apiBaseUrl`, `host.auth`, `host.sdk`, `host.navigation`,
`host.notifications`, `host.telemetry`, `host.breadcrumbs`) — grouped so new capabilities extend the
handle without changing `Root`'s signature. Destructure what you use. All are
backed by Studio's own singletons: `notifications` fires into Studio's shared
Expand All @@ -57,6 +57,12 @@ declares a minimal structural `PluginSdk` covering only the hooks this example
calls. A real plugin either mirrors the calls it needs the same way or, if it can
resolve the SDK's types, types `host.sdk` as Studio does.

`host.sdk` covers **platform** services only. A plugin that calls *its own*
service ships its own client, and must prefix every request with
`host.apiBaseUrl` — Studio's dev-server `/apis` proxy is opt-in, so a bare
`/apis/...` request hits the dev server rather than the platform whenever
`VITE_PLATFORM_BASE_URL` is set.

## Shared UI (`@nemo/common`)

Studio's own table, form, and status components are shared the same way KUI is.
Expand Down Expand Up @@ -103,6 +109,7 @@ import { StudioDataView, useStudioDataViewState } from '@nemo/common';
{
host: {
workspaceId: string;
apiBaseUrl: string;
auth: { accessToken: string; getAccessToken: () => string };
sdk: { platform: /* @nemo/sdk platform hooks */ };
navigation: { navigate: (to: string) => void; back: () => void };
Expand Down
2 changes: 2 additions & 0 deletions plugins/example-plugin/web/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface PluginTelemetry {

export interface PluginHost {
workspaceId: string;
/** Origin the platform API is served from; empty when same-origin. */
apiBaseUrl: string;
auth: {
accessToken: string;
getAccessToken: () => string;
Expand Down
5 changes: 5 additions & 0 deletions web/packages/common/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

export { AccessibleTitle } from '@nemo/common/src/components/AccessibleTitle';
export { AccordionSection } from '@nemo/common/src/components/AccordionSection';
// CancelJobButton is deliberately NOT exported
export { ConfirmationModal } from '@nemo/common/src/components/ConfirmationModal';
export { DeleteConfirmationModal } from '@nemo/common/src/components/DeleteConfirmationModal';
export type { AccordionSectionProps } from '@nemo/common/src/components/AccordionSection';
// ErrorPanel is deliberately NOT exported
export { ExpandableMessage } from '@nemo/common/src/components/ExpandableMessage';
export { FileTag } from '@nemo/common/src/components/FileTag';
export type { FileTagProps, FileTagStatus } from '@nemo/common/src/components/FileTag';
Expand Down Expand Up @@ -41,6 +43,7 @@ export type { BadgeStatus, StatusConfigEntry } from '@nemo/common/src/components
export { TableEmptyState } from '@nemo/common/src/components/TableEmptyState';

export { ControlledSelect } from '@nemo/common/src/components/form/ControlledSelect';
export { ControlledTextArea } from '@nemo/common/src/components/form/ControlledTextArea';
export { ControlledTextInput } from '@nemo/common/src/components/form/ControlledTextInput';

export { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState';
Expand All @@ -60,6 +63,8 @@ export {
} from '@nemo/common/src/utils/query';
export { triggerDownload } from '@nemo/common/src/utils/file';

export { ENTITY_NAME_HELP, entityNameSchema } from '@nemo/common/src/utils/entityName';

export { getErrorMessage } from '@nemo/common/src/utils/error';
export { handleFormErrorsGeneric } from '@nemo/common/src/utils/forms/error';
export { logger, toError } from '@nemo/common/src/utils/logger';
Expand Down
3 changes: 3 additions & 0 deletions web/packages/studio/src/plugins/PluginRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ describe('PluginRenderer', () => {
expect(typeof capturedProps?.host.navigation.navigate).toBe('function');
expect(typeof capturedProps?.host.notifications.notify).toBe('function');
expect(typeof capturedProps?.host.telemetry.event).toBe('function');
// A plugin calling its own service prefixes requests with this; it must be a
// string even when unset, so `${apiBaseUrl}${path}` never yields "undefined/...".
expect(typeof capturedProps?.host.apiBaseUrl).toBe('string');
});

it('does not remount on token renewal and getAccessToken returns the new token', () => {
Expand Down
2 changes: 2 additions & 0 deletions web/packages/studio/src/plugins/PluginRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { useToast } from '@nemo/common/src/providers/toast/useToast';
import { logger } from '@nemo/common/src/utils/logger';
import * as platformSdk from '@nemo/sdk/generated/platform/api';
import { PLATFORM_BASE_URL } from '@studio/constants/environment';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { usePlugins, usePluginsLoaded } from '@studio/plugins/PluginContext';
import { PluginErrorBoundary } from '@studio/plugins/PluginErrorBoundary';
Expand Down Expand Up @@ -61,6 +62,7 @@ export const PluginRenderer = (): ReactElement => {
const host = useMemo<PluginHost>(
() => ({
workspaceId: workspace,
apiBaseUrl: PLATFORM_BASE_URL ?? '',
auth: { accessToken, getAccessToken },
sdk: STUDIO_SDK,
navigation: { navigate: (to) => navigate(to), back: () => navigate(-1) },
Expand Down
7 changes: 7 additions & 0 deletions web/packages/studio/src/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ export interface PluginTelemetry {
/** The host handle Studio injects into every plugin; extend it to add capabilities. */
export interface PluginHost {
workspaceId: string;
/**
* Origin the platform API is served from; empty when same-origin. A plugin
* that calls its own service needs this: Studio's dev-server `/apis` proxy is
* opt-in, so a relative request would otherwise hit the dev server whenever
* VITE_PLATFORM_BASE_URL is set.
*/
apiBaseUrl: string;
// Access tokens only — refresh tokens must not cross the boundary.
auth: {
accessToken: string;
Expand Down