From 69c30b90a0f167586fdd4f80e69c174cbf338e3c Mon Sep 17 00:00:00 2001 From: roushan65 Date: Sat, 25 Apr 2026 02:18:16 +0530 Subject: [PATCH 1/7] CONTNM-48: add custom renderer for credential --- .../node-dialog/CredentialRenderer.tsx | 190 ++++++++++++++++++ .../components/node-dialog/NodeDialog.tsx | 2 + .../src/browser/service/CredentialsService.ts | 103 ++++++++++ 3 files changed, 295 insertions(+) create mode 100644 workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx create mode 100644 workflow-editor-extension/src/browser/service/CredentialsService.ts diff --git a/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx b/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx new file mode 100644 index 0000000..3a09845 --- /dev/null +++ b/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import { ControlProps } from '@jsonforms/core'; +import { withJsonFormsControlProps } from '@jsonforms/react'; +import { + Box, + FormHelperText, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, + SelectChangeEvent, + Button, + IconButton, + InputAdornment, + OutlinedInput, +} from '@mui/material'; +import { isControl, rankWith } from '@jsonforms/core'; +import AddIcon from '@mui/icons-material/Add'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { credentialsService, CredentialResponse } from '../../service/CredentialsService'; + +interface CredentialRendererProps extends ControlProps { + options?: { + format?: string; + credentialType?: string; + }; +} + +const CredentialRenderer: React.FC = (props) => { + const { + data, + handleChange, + label, + errors, + options = {}, + visible, + path, + } = props; + + const credentialType = options.credentialType || ''; + const format = options.format || ''; + + const [credentials, setCredentials] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + // Fetch credentials based on type + const fetchCredentials = React.useCallback(async () => { + setLoading(true); + setError(null); + try { + let fetchedCredentials: CredentialResponse[]; + + if (credentialType) { + // Fetch credentials filtered by type + fetchedCredentials = await credentialsService.listByType(credentialType); + } else { + // Fetch all credentials + fetchedCredentials = await credentialsService.list(); + } + + setCredentials(fetchedCredentials); + } catch (err) { + console.error('Failed to fetch credentials:', err); + setError(err instanceof Error ? err.message : 'Failed to load credentials'); + setCredentials([]); + } finally { + setLoading(false); + } + }, [credentialType]); + + // Fetch credentials on mount and when credentialType changes + React.useEffect(() => { + fetchCredentials(); + }, [fetchCredentials]); + + const handleSelectChange = (event: SelectChangeEvent) => { + handleChange(path, event.target.value); + }; + + const handleRefresh = () => { + fetchCredentials(); + }; + + const handleAddNew = () => { + // TODO: Open credential creation dialog/modal + console.log('Add new credential clicked, type:', credentialType); + }; + + if (format !== 'credential') { + return null; + } + + if (!visible) { + return null; + } + + const hasError = Boolean(errors && errors.length > 0); + const errorMessage = hasError ? errors : undefined; + + return ( + + {label && ( + + {label} + + )} + + + + {loading ? 'Loading...' : 'Select Credential'} + + + + + + {error && ( + + {error} + + )} + {errorMessage && ( + + {errorMessage} + + )} + + ); +}; + +const CredentialControl = withJsonFormsControlProps(CredentialRenderer); + +// Tester to determine when to use this renderer +export const credentialTester = rankWith( + 10, + (uischema: any) => { + return ( + isControl(uischema) && + uischema.options?.format === 'credential' + ); + } +); + +export { CredentialControl }; +export default CredentialControl; + + diff --git a/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx b/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx index 6139e64..2f03b4a 100644 --- a/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx +++ b/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx @@ -24,6 +24,7 @@ import { GroupLayout } from '@jsonforms/core'; import CodeEditorControl, { codeEditorTester } from './CodeEditorRenderer'; +import CredentialControl, { credentialTester } from './CredentialRenderer'; /** * Custom Group Layout Renderer @@ -238,6 +239,7 @@ const MIN_DIALOG_HEIGHT = 300; const customRenderers = [ { tester: codeEditorTester, renderer: CodeEditorControl }, + { tester: credentialTester, renderer: CredentialControl }, { tester: categorizationTester, renderer: MaterialCategorizationLayout }, { tester: groupTester, renderer: MaterialGroupLayout }, ...materialRenderers, diff --git a/workflow-editor-extension/src/browser/service/CredentialsService.ts b/workflow-editor-extension/src/browser/service/CredentialsService.ts new file mode 100644 index 0000000..5b2971e --- /dev/null +++ b/workflow-editor-extension/src/browser/service/CredentialsService.ts @@ -0,0 +1,103 @@ +import { API_SERVER_BASE } from './ApiConfig'; + +const API_BASE = `${API_SERVER_BASE}/api/v1/credentials`; + +export interface CredentialResponse { + userId: string; + name: string; + type: string; + typeVersion: string; + description: string | null; + createdAt: string; + updatedAt: string; +} + +export class CredentialsServiceError extends Error { + constructor(public status: number, message: string) { + super(message); + this.name = 'CredentialsServiceError'; + } +} + +async function handleResponse(response: Response): Promise { + if (!response.ok) { + const errorText = await response.text(); + let message = errorText || `HTTP ${response.status}`; + try { + const errorJson = JSON.parse(errorText); + if (errorJson.error) message = errorJson.error; + } catch { + // use raw text + } + throw new CredentialsServiceError(response.status, message); + } + + if (response.status === 204) { + return undefined as T; + } + + return response.json(); +} + +export class CredentialsService { + /** + * List all credentials for the current user + */ + async list(): Promise { + const response = await fetch(API_BASE, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + return handleResponse(response); + } + + /** + * List credentials filtered by type + * @param type The credential type to filter by (e.g., 'BASIC', 'TOKEN') + */ + async listByType(type: string): Promise { + const response = await fetch(`${API_BASE}/type/${encodeURIComponent(type)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + return handleResponse(response); + } + + /** + * List credentials filtered by type and version + * @param type The credential type to filter by (e.g., 'BASIC', 'TOKEN') + * @param version The credential type version + */ + async listByTypeAndVersion(type: string, version: string): Promise { + const response = await fetch(`${API_BASE}/type/${encodeURIComponent(type)}/${encodeURIComponent(version)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + return handleResponse(response); + } + + /** + * Get a credential by name + * @param name The credential name + */ + async getByName(name: string): Promise { + const response = await fetch(`${API_BASE}/${encodeURIComponent(name)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + return handleResponse(response); + } +} + +// Singleton instance +export const credentialsService = new CredentialsService(); + + From f72ce7edb47fa438240e0ecaa9cded5c111f8c0f Mon Sep 17 00:00:00 2001 From: roushan65 Date: Sat, 25 Apr 2026 02:21:54 +0530 Subject: [PATCH 2/7] CONTNM-48: bump tp 0.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df45f52..f98aea4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "continuum-frontend", "private": true, - "version": "0.0.7", + "version": "0.0.8", "scripts": { "build": "turbo build", "rebuild": "turbo build --no-cache --force", From 24b628ccc67d36e66a5682a9b0f8fad08d7c5675 Mon Sep 17 00:00:00 2001 From: roushan65 Date: Sat, 25 Apr 2026 03:10:30 +0530 Subject: [PATCH 3/7] CONTNM-48: use CREDENTIALS_MANAGER_BASE url --- workflow-editor-extension/src/browser/service/ApiConfig.ts | 3 ++- .../src/browser/service/CredentialsService.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/workflow-editor-extension/src/browser/service/ApiConfig.ts b/workflow-editor-extension/src/browser/service/ApiConfig.ts index 93f7e49..dde8bfe 100644 --- a/workflow-editor-extension/src/browser/service/ApiConfig.ts +++ b/workflow-editor-extension/src/browser/service/ApiConfig.ts @@ -9,4 +9,5 @@ const isBehindGateway = /^\/workbench\/[^/]+\/open(\/|$)/.test(window.location.pathname); -export const API_SERVER_BASE = isBehindGateway ? '/api-server' : ''; +export const API_SERVER_BASE = isBehindGateway ? '/api-server' : 'http://localhost:8080/api-server'; +export const CREDENTIALS_MANAGER_BASE = isBehindGateway ? '/credentials-manager': 'http://localhost:8080/credentials-manager'; \ No newline at end of file diff --git a/workflow-editor-extension/src/browser/service/CredentialsService.ts b/workflow-editor-extension/src/browser/service/CredentialsService.ts index 5b2971e..80a5866 100644 --- a/workflow-editor-extension/src/browser/service/CredentialsService.ts +++ b/workflow-editor-extension/src/browser/service/CredentialsService.ts @@ -1,6 +1,6 @@ -import { API_SERVER_BASE } from './ApiConfig'; +import { CREDENTIALS_MANAGER_BASE } from './ApiConfig'; -const API_BASE = `${API_SERVER_BASE}/api/v1/credentials`; +const API_BASE = `${CREDENTIALS_MANAGER_BASE}/api/v1/credentials`; export interface CredentialResponse { userId: string; From 9adf3ec2dae54d59d97b9ac9ef7b0674cd95e3f1 Mon Sep 17 00:00:00 2001 From: roushan65 Date: Sat, 25 Apr 2026 10:02:08 +0530 Subject: [PATCH 4/7] CONTNM-48: fetch credentials by type --- .../node-dialog/CredentialRenderer.tsx | 135 +++++++++++++----- .../components/node-dialog/NodeDialog.tsx | 2 + 2 files changed, 105 insertions(+), 32 deletions(-) diff --git a/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx b/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx index 3a09845..ac5b655 100644 --- a/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx +++ b/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx @@ -11,15 +11,76 @@ import { MenuItem, SelectChangeEvent, Button, - IconButton, - InputAdornment, - OutlinedInput, } from '@mui/material'; import { isControl, rankWith } from '@jsonforms/core'; import AddIcon from '@mui/icons-material/Add'; -import RefreshIcon from '@mui/icons-material/Refresh'; import { credentialsService, CredentialResponse } from '../../service/CredentialsService'; +// Mock credentials for local testing - set to true to use mock data +const USE_MOCK_DATA = false; + +const MOCK_CREDENTIALS: Record = { + BASIC: [ + { + userId: 'anonymous', + name: 'admin-basic-auth', + type: 'BASIC', + typeVersion: '1.0.0', + description: 'Admin credentials for internal APIs', + createdAt: '2026-04-24T10:00:00Z', + updatedAt: '2026-04-24T10:00:00Z', + }, + { + userId: 'anonymous', + name: 'service-account', + type: 'BASIC', + typeVersion: '1.0.0', + description: 'Service account for batch jobs', + createdAt: '2026-04-24T11:00:00Z', + updatedAt: '2026-04-24T11:00:00Z', + }, + ], + TOKEN: [ + { + userId: 'anonymous', + name: 'github-token', + type: 'TOKEN', + typeVersion: '1.0.0', + description: 'GitHub API access token', + createdAt: '2026-04-24T12:00:00Z', + updatedAt: '2026-04-24T12:00:00Z', + }, + { + userId: 'anonymous', + name: 'openai-api-key', + type: 'TOKEN', + typeVersion: '1.0.0', + description: 'OpenAI API key for LLM calls', + createdAt: '2026-04-24T13:00:00Z', + updatedAt: '2026-04-24T13:00:00Z', + }, + ], + GENERIC: [ + { + userId: 'anonymous', + name: 'basic-api-creds', + type: 'GENERIC', + typeVersion: '1.0.0', + description: 'Generic API credentials', + createdAt: '2026-04-24T19:14:21Z', + updatedAt: '2026-04-24T19:14:21Z', + }, + ], +}; + +const getMockCredentials = (type?: string): CredentialResponse[] => { + if (type) { + return MOCK_CREDENTIALS[type] || []; + } + // Return all credentials + return Object.values(MOCK_CREDENTIALS).flat(); +}; + interface CredentialRendererProps extends ControlProps { options?: { format?: string; @@ -33,14 +94,27 @@ const CredentialRenderer: React.FC = (props) => { handleChange, label, errors, - options = {}, visible, path, + uischema, } = props; + // Options are in uischema.options, not directly in props + const options = (uischema as any)?.options || {}; const credentialType = options.credentialType || ''; const format = options.format || ''; + console.log('[CredentialRenderer] Render called with props:', { + path, + label, + format, + credentialType, + visible, + data, + options, + uischema, + }); + const [credentials, setCredentials] = React.useState([]); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); @@ -52,7 +126,11 @@ const CredentialRenderer: React.FC = (props) => { try { let fetchedCredentials: CredentialResponse[]; - if (credentialType) { + if (USE_MOCK_DATA) { + // Use mock data for local testing + await new Promise(resolve => setTimeout(resolve, 300)); // Simulate network delay + fetchedCredentials = getMockCredentials(credentialType); + } else if (credentialType) { // Fetch credentials filtered by type fetchedCredentials = await credentialsService.listByType(credentialType); } else { @@ -79,23 +157,23 @@ const CredentialRenderer: React.FC = (props) => { handleChange(path, event.target.value); }; - const handleRefresh = () => { - fetchCredentials(); - }; - const handleAddNew = () => { // TODO: Open credential creation dialog/modal console.log('Add new credential clicked, type:', credentialType); }; if (format !== 'credential') { + console.log('[CredentialRenderer] Returning null - format is not "credential":', format); return null; } if (!visible) { + console.log('[CredentialRenderer] Returning null - not visible'); return null; } + console.log('[CredentialRenderer] Rendering component with credentials:', credentials); + const hasError = Boolean(errors && errors.length > 0); const errorMessage = hasError ? errors : undefined; @@ -118,24 +196,11 @@ const CredentialRenderer: React.FC = (props) => { label={loading ? 'Loading...' : 'Select Credential'} onChange={handleSelectChange} disabled={loading} - input={ - - - - - - } - /> - } + MenuProps={{ + PaperProps: { + sx: { zIndex: 9999 } + } + }} > None @@ -177,10 +242,16 @@ const CredentialControl = withJsonFormsControlProps(CredentialRenderer); export const credentialTester = rankWith( 10, (uischema: any) => { - return ( - isControl(uischema) && - uischema.options?.format === 'credential' - ); + const isMatch = isControl(uischema) && uischema.options?.format === 'credential'; + console.log('[credentialTester] Testing uischema:', { + type: uischema?.type, + scope: uischema?.scope, + options: uischema?.options, + isControl: isControl(uischema), + hasCredentialFormat: uischema?.options?.format === 'credential', + isMatch, + }); + return isMatch; } ); diff --git a/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx b/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx index 2f03b4a..fc866c7 100644 --- a/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx +++ b/workflow-editor-extension/src/browser/components/node-dialog/NodeDialog.tsx @@ -237,6 +237,8 @@ const ResizeHandle = styled('div')(({ theme }) => ({ const MIN_DIALOG_WIDTH = 400; const MIN_DIALOG_HEIGHT = 300; +console.log('[NodeDialog] Registering custom renderers including CredentialControl'); + const customRenderers = [ { tester: codeEditorTester, renderer: CodeEditorControl }, { tester: credentialTester, renderer: CredentialControl }, From 01a4394fa1f6802f1d80b6d42a2d824add8610c0 Mon Sep 17 00:00:00 2001 From: roushan65 Date: Sat, 25 Apr 2026 10:37:21 +0530 Subject: [PATCH 5/7] CONTNM-48: Also add a refresh icon button --- .../node-dialog/CredentialRenderer.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx b/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx index ac5b655..a2253b5 100644 --- a/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx +++ b/workflow-editor-extension/src/browser/components/node-dialog/CredentialRenderer.tsx @@ -14,6 +14,7 @@ import { } from '@mui/material'; import { isControl, rankWith } from '@jsonforms/core'; import AddIcon from '@mui/icons-material/Add'; +import RefreshIcon from '@mui/icons-material/Refresh'; import { credentialsService, CredentialResponse } from '../../service/CredentialsService'; // Mock credentials for local testing - set to true to use mock data @@ -148,10 +149,12 @@ const CredentialRenderer: React.FC = (props) => { } }, [credentialType]); - // Fetch credentials on mount and when credentialType changes + // Fetch credentials only when visible and credentialType changes React.useEffect(() => { - fetchCredentials(); - }, [fetchCredentials]); + if (visible && format === 'credential') { + fetchCredentials(); + } + }, [fetchCredentials, visible, format]); const handleSelectChange = (event: SelectChangeEvent) => { handleChange(path, event.target.value); @@ -212,6 +215,16 @@ const CredentialRenderer: React.FC = (props) => { ))} +