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
2 changes: 1 addition & 1 deletion console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"i18next": "^25.3.2",
"jcode-ui": "file:../../jcode/packages/jcode-ui",
"jcode-ui-core": "file:../../jcode/packages/jcode-ui-core",
"jtype-board-react": "0.1.0",
"jtype-board-react": "github:cnjack/jtype#939d7177676376942a8059b096d2c26205472239&path:/packages/board-react",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.6.1",
Expand Down
29 changes: 6 additions & 23 deletions console/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions console/src/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ describe('httpClient — request shaping', () => {
expect(JSON.parse(calls[0]!.init!.body as string)).toEqual({ prompt: 'do it' });
});

it('encodes Card execution scope and opaque pagination cursor', async () => {
const { calls } = mockFetch(() => ({
body: { claim: null, items: [], next_cursor: null },
}));
const client = createHttpClient('t');
await client.listServiceKanbanCardExecutions(
'service/1', 'workspace 1', 'cards/payment fix.md', 'opaque+/cursor', 12,
);
const url = new URL(calls[0]!.url, 'https://console.test');
expect(url.pathname).toBe('/api/v1/services/service%2F1/kanban/card-executions');
expect(url.searchParams.get('workspace_id')).toBe('workspace 1');
expect(url.searchParams.get('document_path')).toBe('cards/payment fix.md');
expect(url.searchParams.get('before')).toBe('opaque+/cursor');
expect(url.searchParams.get('limit')).toBe('12');
});

it('stages an attachment as JSON and uploads its raw body through Cloud', async () => {
const { calls } = mockFetch(({ url }) => {
if (url.endsWith('/attachments/intents')) {
Expand Down
16 changes: 16 additions & 0 deletions console/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import type {
GitHubAppInstallation,
GitHubInstallationConsentPreview,
JTypePluginConnectStatus,
KanbanCardExecutionsPage,
ProviderKind,
PluginRepositoryResource,
PluginWorkspaceResource,
Expand Down Expand Up @@ -77,6 +78,7 @@ import type {
UpdateProjectAutomationInput,
PutServiceKanbanInput,
ServiceKanbanBinding,
ServiceKanbanPolicy,
UpdateModelInput,
UpdateModelProviderInput,
UpdateProjectInput,
Expand Down Expand Up @@ -336,6 +338,8 @@ export interface ApiClient {
updateProjectAutomation(projectId: string, automationId: string, input: UpdateProjectAutomationInput): Promise<ProjectAutomationSpec>;
deleteProjectAutomation(projectId: string, automationId: string): Promise<void>;
getServiceKanban(serviceId: string): Promise<ServiceKanbanBinding>;
getServiceKanbanPolicy(serviceId: string): Promise<ServiceKanbanPolicy>;
listServiceKanbanCardExecutions(serviceId: string, workspaceId: string, documentPath: string, before?: string, limit?: number): Promise<KanbanCardExecutionsPage>;
putServiceKanban(serviceId: string, input: PutServiceKanbanInput): Promise<ServiceKanbanBinding>;
deleteServiceKanban(serviceId: string): Promise<void>;
/** Lists branches through the Service's bound Plugin; never exposes a Git credential. */
Expand Down Expand Up @@ -824,6 +828,18 @@ export function createHttpClient(
deleteProjectAutomation: (_projectId, automationId) =>
req<void>(`/automations/${encodeURIComponent(automationId)}`, { method: 'DELETE' }),
getServiceKanban: (serviceId) => req<ServiceKanbanBinding>(`/services/${encodeURIComponent(serviceId)}/kanban`),
getServiceKanbanPolicy: (serviceId) => req<ServiceKanbanPolicy>(`/services/${encodeURIComponent(serviceId)}/kanban/policy`),
listServiceKanbanCardExecutions: (serviceId, workspaceId, documentPath, before, limit = 20) => {
const params = new URLSearchParams({
workspace_id: workspaceId,
document_path: documentPath,
limit: String(limit),
});
if (before) params.set('before', before);
return req<KanbanCardExecutionsPage>(
`/services/${encodeURIComponent(serviceId)}/kanban/card-executions?${params.toString()}`,
);
},
putServiceKanban: (serviceId, input) => req<ServiceKanbanBinding>(`/services/${encodeURIComponent(serviceId)}/kanban`, {
method: 'PUT', body: JSON.stringify(input),
}),
Expand Down
31 changes: 31 additions & 0 deletions console/src/api/mockClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ import type {
ResumeSessionOptions,
RunStatus,
Service,
KanbanCardExecutionsPage,
ServiceKanbanPolicy,
ServiceBranch,
SystemInfo,
UpdateClusterProviderConfigInput,
Expand Down Expand Up @@ -2175,6 +2177,35 @@ export function createMockClient(): ApiClient {
if (!spec) throw new ApiError(404, 'Kanban is not enabled');
return delay(structuredClone(spec));
},
async getServiceKanbanPolicy(serviceId: string): Promise<ServiceKanbanPolicy> {
const service = [...services.values()].flat().find((item) => item.id === serviceId);
const spec = [...projectAutomations.values()].find((item) =>
item.automation.service_id === serviceId && item.automation.trigger_kind === 'kanban');
if (!service || !spec?.kanban) throw new ApiError(404, 'Kanban is not enabled');
const plugin = [...pluginList(service.project_id).values()].find((item) => item.id === spec.kanban!.installation_id);
return delay({
service_id: serviceId,
service_name: service.name,
repository: service.repo_owner_name ?? service.raw_repo_url ?? '',
model: { label: 'Demo model' },
board: { workspace_id: plugin?.workspace_id ?? '', ref: spec.kanban.board_ref },
trigger_column: { key: spec.kanban.trigger_column, label: spec.kanban.trigger_column },
done_column: { key: spec.kanban.done_column, label: spec.kanban.done_column },
output: spec.kanban.done_column ? 'comment_and_move_on_success' : 'comment_only',
health: {
state: spec.automation.enabled ? 'ready' : 'blocked',
blocker: spec.automation.enabled ? null : 'binding_disabled',
repair_role: spec.automation.enabled ? null : 'project_owner',
},
});
},
async listServiceKanbanCardExecutions(_serviceId: string, _workspaceId: string, _documentPath: string): Promise<KanbanCardExecutionsPage> {
return delay({
claim: null,
items: [],
next_cursor: null,
});
},
async putServiceKanban(serviceId, input): Promise<ProjectAutomationSpec> {
const projectEntry = [...services.entries()].find(([, list]) => list.some((service) => service.id === serviceId));
if (!projectEntry) throw new ApiError(404, 'service not found');
Expand Down
40 changes: 40 additions & 0 deletions console/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* centralised so SSE/status changes can invalidate precisely.
*/
import {
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
Expand Down Expand Up @@ -60,6 +61,9 @@ export const qk = {
// button + feeds the embed modal's selector.
projectBoardLinks: (projectId: string) => ['project-board-links', projectId] as const,
serviceKanban: (serviceId: string) => ['service-kanban', serviceId] as const,
serviceKanbanPolicy: (serviceId: string) => ['service-kanban-policy', serviceId] as const,
serviceKanbanCardExecutions: (serviceId: string, workspaceId: string, documentPath: string) =>
['service-kanban-card-executions', serviceId, workspaceId, documentPath] as const,
serviceBranches: (serviceId: string) => ['service-branches', serviceId] as const,
projectPlugins: (projectId: string) => ['project-plugins', projectId] as const,
projectPluginImpact: (projectId: string, installationId: string) =>
Expand Down Expand Up @@ -760,6 +764,42 @@ export function usePutServiceKanban(projectId: string, serviceId: string) {
});
}

export function useServiceKanbanPolicy(serviceId: string, enabled = true) {
const api = useApi();
return useQuery({
queryKey: qk.serviceKanbanPolicy(serviceId),
queryFn: () => api.getServiceKanbanPolicy(serviceId),
enabled: enabled && !!serviceId,
retry: false,
});
}

export function useServiceKanbanCardExecutions(
serviceId: string,
workspaceId: string,
documentPath: string,
enabled = true,
) {
const api = useApi();
return useInfiniteQuery({
queryKey: qk.serviceKanbanCardExecutions(serviceId, workspaceId, documentPath),
queryFn: ({ pageParam }) => api.listServiceKanbanCardExecutions(
serviceId, workspaceId, documentPath, pageParam ?? undefined,
),
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
enabled: enabled && !!serviceId && !!workspaceId && !!documentPath,
retry: false,
refetchInterval: (query) => {
const pages = query.state.data?.pages;
return pages?.some((page) => page.items.some((item) =>
item.status === 'received' || item.status === 'blocked' || item.status === 'queued' || item.status === 'running'))
? 5_000
: false;
},
});
}

export function useDeleteServiceKanban(projectId: string, serviceId: string) {
const api = useApi();
const qc = useQueryClient();
Expand Down
48 changes: 47 additions & 1 deletion console/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,56 @@ export interface ProjectAutomationSpec {
automation: ProjectAutomationAggregate;
scm?: { automation_id?: string; branch?: string; path_pattern?: string; conclusion?: string; include_drafts?: boolean };
actions?: ScmAutomationAction[];
kanban?: { automation_id?: string; installation_id: string; board_ref: string; trigger_column: string; done_column?: string };
kanban?: {
automation_id?: string;
installation_id: string;
board_ref: string;
trigger_column: string;
trigger_label?: string;
done_column?: string;
done_label?: string;
};
cron?: { automation_id?: string; cron_expr: string };
}
export type ServiceKanbanBinding = ProjectAutomationSpec;
export interface ServiceKanbanPolicy {
service_id: string;
service_name: string;
repository: string;
model: { id?: string; label: string };
board: { workspace_id: string; ref: string };
trigger_column: { key: string; label: string };
done_column: { key?: string; label?: string };
output: 'comment_only' | 'comment_and_move_on_success';
health: {
state: 'ready' | 'blocked';
blocker: string | null;
repair_role?: 'project_owner' | 'cluster_admin' | null;
};
}
export interface KanbanCardExecution {
id: string;
status: 'received' | 'blocked' | 'queued' | 'running' | 'terminal';
outcome?: 'succeeded' | 'failed' | 'canceled';
summary: string;
reason: string | null;
reason_code?: string;
repair_role: 'project_owner' | 'cluster_admin' | null;
requested_actor: { label: string; precision: 'display_only' } | null;
run: { id: string; status: RunStatus; href: string } | null;
receipt: {
external: 'not_required' | 'pending' | 'written' | 'unavailable';
writeback: 'not_required' | 'pending' | 'complete' | 'unavailable';
};
created_at: string;
updated_at: string;
terminal_at?: string;
}
export interface KanbanCardExecutionsPage {
claim: { document_path: string; external_ref_available: boolean } | null;
items: KanbanCardExecution[];
next_cursor: string | null;
}
export interface PutServiceKanbanInput {
installation_id: string;
board_ref: string;
Expand Down
Loading
Loading