Skip to content
Open
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
25 changes: 20 additions & 5 deletions extensions/positron-connections/src/drivers/bigquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class PythonBigQueryDriverBase implements positron.ConnectionsDriver {
positron.RuntimeErrorBehavior.Continue
);
if (!exec) {
throw new Error('Failed to execute code');
throw new Error(vscode.l10n.t('Failed to execute code'));
}
return;
}
Expand Down Expand Up @@ -78,16 +78,22 @@ export class PythonBigQueryDefaultCredentialsDriver extends PythonBigQueryDriver
]
};

generateCode(inputs: positron.ConnectionsInput[]) {
generateCode(inputs: positron.ConnectionsInput[]): string | { code: string; errorMessage: string } {
const project = inputs.find(input => input.id === 'project')?.value ?? '';

return `from google.cloud import bigquery
const code = `from google.cloud import bigquery

# To configure credentials locally, run: gcloud auth application-default login
# See: https://cloud.google.com/docs/authentication/provide-credentials-adc
conn = bigquery.Client(project=${JSON.stringify(project)})
%connection_show conn
`;

if (project === '') {
return { code, errorMessage: vscode.l10n.t('Project ID is required') };
}

return code;
}
}

Expand Down Expand Up @@ -129,16 +135,25 @@ export class PythonBigQueryServiceAccountDriver extends PythonBigQueryDriverBase
]
};

generateCode(inputs: positron.ConnectionsInput[]) {
generateCode(inputs: positron.ConnectionsInput[]): string | { code: string; errorMessage: string } {
const project = inputs.find(input => input.id === 'project')?.value ?? '';
const keyfilePath = inputs.find(input => input.id === 'keyfile_path')?.value ?? '';

return `from google.cloud import bigquery
const code = `from google.cloud import bigquery
from google.oauth2 import service_account

credentials = service_account.Credentials.from_service_account_file(${JSON.stringify(keyfilePath)})
conn = bigquery.Client(credentials=credentials, project=${JSON.stringify(project)})
%connection_show conn
`;

if (project === '') {
return { code, errorMessage: vscode.l10n.t('Project ID is required') };
}

if (keyfilePath === '') {
return { code, errorMessage: vscode.l10n.t('Service account keyfile path is required') };
}
return code;
}
}
23 changes: 21 additions & 2 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1610,8 +1610,27 @@ declare module 'positron' {

/**
* Generates the connection code based on the inputs.
*/
generateCode?: (inputs: Array<ConnectionsInput>) => string;
*
* @param inputs The current values of the connection inputs defined in metadata.
* @returns Either a string containing valid connection code, or an object with:
* - `code`: The generated connection code. Should still be generated even when
* validation fails, so users can see and copy the partial code.
* - `errorMessage`: A user-facing message explaining the validation error,
* displayed in an error banner overlay on the code editor. The Connect
* button is disabled when an error message is present.
*
* @example
* // Return valid code as a string
* generateCode: (inputs) => `library(DBI)\ncon <- dbConnect(...)`
*
* @example
* // Return validation error with generated code
* generateCode: (inputs) => ({
* code: `library(bigrquery)\ncon <- dbConnect(...)`,
* errorMessage: 'Project ID is required'
* })
*/
generateCode?: (inputs: Array<ConnectionsInput>) => string | { code: string; errorMessage: string };

/**
* Connect session.
Expand Down
47 changes: 21 additions & 26 deletions src/vs/workbench/api/browser/positron/mainThreadConnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,41 +36,36 @@ export class MainThreadConnections implements MainThreadConnectionsShape {
}

export interface IAvailableDriverMethods {
generateCode: boolean,
connect: boolean,
checkDependencies: boolean,
installDependencies: boolean
generateCode: boolean;
connect: boolean;
checkDependencies: boolean;
installDependencies: boolean;
}

class MainThreadDriverAdapter implements IDriver {
readonly generateCode?: (inputs: Input[]) => Promise<string | { code: string; errorMessage: string }>;
readonly connect?: (code: string) => Promise<void>;
readonly checkDependencies?: () => Promise<boolean>;
readonly installDependencies?: () => Promise<boolean>;

constructor(
readonly driverId: string,
readonly metadata: IDriverMetadata,
private readonly availableMethods: IAvailableDriverMethods,
private readonly _proxy: ExtHostConnectionsShape
) { }
get generateCode() {
if (!this.availableMethods.generateCode) {
return undefined;
availableMethods: IAvailableDriverMethods,
proxy: ExtHostConnectionsShape
) {
// Create stable function references once in the constructor
if (availableMethods.generateCode) {
this.generateCode = (inputs: Input[]) => proxy.$driverGenerateCode(driverId, inputs);
}
return (inputs: Input[]) => this._proxy.$driverGenerateCode(this.driverId, inputs);
}
get connect() {
if (!this.availableMethods.connect) {
return undefined;
if (availableMethods.connect) {
this.connect = (code: string) => proxy.$driverConnect(driverId, code);
}
return (code: string) => this._proxy.$driverConnect(this.driverId, code);
}
get checkDependencies() {
if (!this.availableMethods.checkDependencies) {
return undefined;
if (availableMethods.checkDependencies) {
this.checkDependencies = () => proxy.$driverCheckDependencies(driverId);
}
return () => this._proxy.$driverCheckDependencies(this.driverId);
}
get installDependencies() {
if (!this.availableMethods.installDependencies) {
return undefined;
if (availableMethods.installDependencies) {
this.installDependencies = () => proxy.$driverInstallDependencies(driverId);
}
return () => this._proxy.$driverInstallDependencies(this.driverId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export interface MainThreadConnectionsShape {
}

export interface ExtHostConnectionsShape {
$driverGenerateCode(driverId: string, inputs: Input[]): Promise<string>;
$driverGenerateCode(driverId: string, inputs: Input[]): Promise<string | { code: string; errorMessage: string }>;
$driverConnect(driverId: string, code: string): Promise<void>;
$driverCheckDependencies(driverId: string): Promise<boolean>;
$driverInstallDependencies(driverId: string): Promise<boolean>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@
display: flex;
flex-direction: row;
align-items: center;
}

.create-connection-footer > .default {
margin-left: auto;
justify-content: space-between;
}

.create-connection-inputs {
Expand Down Expand Up @@ -113,3 +110,40 @@
flex: 1;
min-width: 0;
}

/* Error state for code editor */
.create-connection-code-editor {
position: relative;
}

.connection-error-message {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 4px 8px;
font-size: 12px;
background-color: var(--vscode-inputValidation-errorBackground);
border-top: 1px solid var(--vscode-inputValidation-errorBorder);
color: var(--vscode-inputValidation-errorForeground);
transform-origin: bottom;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 6px;
transform: scaleY(0);
opacity: 0;
transition: transform 0.2s ease-out, opacity 0.2s ease-out;
}

.create-connection-code-editor.has-error .connection-error-message {
transform: scaleY(1);
opacity: 1;
}

.connection-error-message > .codicon.codicon-error {
color: var(--vscode-inputValidation-errorBorder);
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { DropDownListBox } from '../../../../../browser/positronComponents/dropD
import { DropDownListBoxItem } from '../../../../../browser/positronComponents/dropDownListBox/dropDownListBoxItem.js';
import { usePositronReactServicesContext } from '../../../../../../base/browser/positronReactRendererContext.js';
import { PositronModalReactRenderer } from '../../../../../../base/browser/positronModalReactRenderer.js';
import { Icon } from '../../../../../../platform/positronActionBar/browser/components/icon.js';
import { positronClassNames } from '../../../../../../base/common/positronUtilities.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';

interface CreateConnectionProps {
readonly renderer: PositronModalReactRenderer;
Expand All @@ -37,18 +41,27 @@ export const CreateConnection = (props: PropsWithChildren<CreateConnectionProps>
const editorRef = useRef<SimpleCodeEditorWidget>(undefined!);

const [inputs, setInputs] = useState<Array<Input>>(metadata.inputs);
const [code, setCode] = useState<string | undefined>(undefined);
const [codeState, setCodeState] = useState<{ code: string; errorMessage?: string } | undefined>(undefined);

const editorOptions: IEditorOptions = {
readOnly: true,
cursorBlinking: 'solid' as const
Copy link
Contributor

Choose a reason for hiding this comment

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

small nit: I don't think you need the as const here anymore now that this is typed!

};

useEffect(() => {
// Debounce the code generation to avoid unnecessary re-renders
const timeoutId = setTimeout(async () => {
if (generateCode) {
const code = await generateCode(inputs);
setCode(code);
const result = await generateCode(inputs);
if (typeof result === 'string') {
setCodeState({ code: result });
} else {
setCodeState(result);
}
}
}, 200);
return () => clearTimeout(timeoutId);
}, [inputs, generateCode, setCode]);
}, [inputs, generateCode, setCodeState]);

const onConnectHandler = async () => {
// Acquire code before disposing of the renderer
Expand Down Expand Up @@ -118,23 +131,24 @@ export const CreateConnection = (props: PropsWithChildren<CreateConnectionProps>
</h1>
</div>

<Form inputs={metadata.inputs} onInputsChange={setInputs}></Form>
<Form inputs={inputs} onInputsChange={setInputs}></Form>

<div className='create-connection-code-title'>
{(() => localize('positron.newConnectionModalDialog.createConnection.code', "Connection Code"))()}
</div>

<div className='create-connection-code-editor'>
<div className={positronClassNames('create-connection-code-editor', { 'has-error': !!codeState?.errorMessage })}>
<SimpleCodeEditor
ref={editorRef}
code={code}
editorOptions={{
readOnly: true,
cursorBlinking: 'solid'
}}
code={codeState?.code || ''}
editorOptions={editorOptions}
language={languageId}
>
</SimpleCodeEditor>
<div className='connection-error-message'>
<Icon icon={Codicon.error} />
{codeState?.errorMessage}
</div>
</div>

<div className='create-connection-buttons'>
Expand All @@ -160,7 +174,8 @@ export const CreateConnection = (props: PropsWithChildren<CreateConnectionProps>
{(() => localize('positron.newConnectionModalDialog.createConnection.back', 'Back'))()}
</PositronButton>
<PositronButton
className='button action-bar-button default'
className={`button action-bar-button`}
disabled={!codeState || !!codeState.errorMessage}
onPressed={onConnectHandler}
>
{(() => localize('positron.newConnectionModalDialog.createConnection.connect', 'Connect'))()}
Expand All @@ -185,12 +200,9 @@ const Form = (props: PropsWithChildren<{ inputs: Input[], onInputsChange: (input
inputs.map((input) => {
return <FormElement key={input.id} input={input} onChange={(value) => {
onInputsChange(
inputs.map((i) => {
if (i.id === input.id) {
i.value = value;
}
return i;
})
inputs.map((i) =>
i.id === input.id ? { ...i, value } : i
)
);
}}></FormElement>;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export interface IDriver {
metadata: IDriverMetadata;

// Generates the connection code based on the inputs.
generateCode?: (inputs: Array<Input>) => Promise<string>;
// Returns a string for valid code, or an object with code and errorMessage if validation fails.
generateCode?: (inputs: Array<Input>) => Promise<string | { code: string; errorMessage: string }>;
// Connect session
connect?: (code: string) => Promise<void>;
// Checks if the dependencies for the driver are installed
Expand Down