Skip to content

Commit

Permalink
Provide back button navigation in new cluster wizard
Browse files Browse the repository at this point in the history
Fixes #21

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jan 5, 2021
1 parent e2f0b8f commit 3c8ef1e
Show file tree
Hide file tree
Showing 8 changed files with 657 additions and 109 deletions.
46 changes: 4 additions & 42 deletions src/commands/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,24 @@
import * as vscode from "vscode";
import { dump } from "js-yaml";
import { Broker, ClientAccessor, SaslOption } from "../client";
import { Broker, ClientAccessor } from "../client";
import { BrokerItem } from "../explorer/models/brokers";
import { OutputChannelProvider } from "../providers";
import { pickBroker, pickCluster } from "./common";
import { ClusterSettings } from "../settings";
import { KafkaExplorer } from "../explorer";
import { addClusterWizard } from "../wizards/clusters";

/**
* Adds a new cluster to the collection.
*/
export class AddClusterCommandHandler {
private readonly AuthOptions = ["None", "SASL/PLAIN"];

constructor(private clusterSettings: ClusterSettings, private explorer: KafkaExplorer) {
}

async execute(): Promise<void> {
const bootstrap = await vscode.window.showInputBox({ placeHolder: "Broker(s) (localhost:9092,localhost:9093...)", ignoreFocusOut: true });

if (!bootstrap) {
return;
}

const name = await vscode.window.showInputBox({ placeHolder: "Friendly name", ignoreFocusOut: true });

if (!name) {
return;
}

const pickedAuthOption = await vscode.window.showQuickPick(this.AuthOptions, { placeHolder: "Authentication", ignoreFocusOut: true });
let saslOption: SaslOption | undefined;

if (pickedAuthOption && pickedAuthOption === this.AuthOptions[1]) {
const username = await vscode.window.showInputBox({ placeHolder: "Username", ignoreFocusOut: true });
const password = await vscode.window.showInputBox({ placeHolder: "Password", password: true, ignoreFocusOut: true });

if (username && password) {
saslOption = {
mechanism: "plain",
username,
password,
};
}
}

const sanitizedName = name.replace(/[^a-zA-Z0-9]/g, "");
const suffix = Buffer.from(bootstrap).toString("base64").replace(/=/g, "");

this.clusterSettings.upsert({
id: `${sanitizedName}-${suffix}`,
bootstrap,
name,
saslOption,
});

this.explorer.refresh();
addClusterWizard(this.clusterSettings, this.explorer);
}

}

/**
Expand Down
71 changes: 6 additions & 65 deletions src/commands/topics.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,22 @@
import { ClusterSettings } from "../settings";
import { dump } from "js-yaml";
import * as vscode from "vscode";

import { Topic, ClientAccessor, Client } from "../client";
import { KafkaExplorer, TopicItem } from "../explorer";
import { OutputChannelProvider } from "../providers";
import { addTopicWizard } from "../wizards/topics";
import { pickTopicFromSelectedCluster } from "./common";

const AUTO_CREATE_TOPIC_KEY = 'auto.create.topics.enable';

export class CreateTopicCommandHandler {
constructor(private clientAccessor: ClientAccessor, private explorer: KafkaExplorer) {
}

private validatePositiveNumber(value?: string): string | undefined {
if (!value) {
return "Must be a positive number";
}

const valueAsNumber = parseInt(value, 10);

if (isNaN(valueAsNumber) || valueAsNumber < 1) {
return "Must be a positive number";
}
constructor(private clientAccessor: ClientAccessor, private clusterSettings : ClusterSettings, private explorer: KafkaExplorer) {
}

async execute(clusterId?: string): Promise<void> {
if (!clusterId) {
return;
}

const topic = await vscode.window.showInputBox({ placeHolder: "Topic name", ignoreFocusOut: true });

if (!topic) {
return;
}

const partitions = await vscode.window.showInputBox({
placeHolder: "Number of partitions",
validateInput: this.validatePositiveNumber,
ignoreFocusOut: true
});

if (!partitions) {
return;
}

const replicationFactor = await vscode.window.showInputBox({
placeHolder: "Replication Factor",
validateInput: this.validatePositiveNumber,
ignoreFocusOut: true
});

if (!replicationFactor) {
return;
}

try {
const client = this.clientAccessor.get(clusterId);
const result = await client.createTopic({
topic,
partitions: parseInt(partitions, 10),
replicationFactor: parseInt(replicationFactor, 10),
});

if (result.length > 0) {
vscode.window.showErrorMessage(result[0].error);
} else {
this.explorer.refresh();
vscode.window.showInformationMessage(`Topic '${topic}' created successfully`);
}
} catch (error) {
if (error.message) {
vscode.window.showErrorMessage(error.message);
} else {
vscode.window.showErrorMessage(error);
}
}
addTopicWizard(this.clientAccessor, this.clusterSettings, this.explorer, clusterId);
}
}

Expand Down Expand Up @@ -164,7 +105,7 @@ export class DeleteTopicCommandHandler {
return;
}

await client.deleteTopic({topics:[ topicToDelete.id ]});
await client.deleteTopic({ topics: [topicToDelete.id] });
this.explorer.refresh();
vscode.window.showInformationMessage(`Topic '${topicToDelete.id}' deleted successfully`);
} catch (error) {
Expand All @@ -175,4 +116,4 @@ export class DeleteTopicCommandHandler {
}
}
}
}
}
3 changes: 2 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from "path";
import { Context } from "./context";

export const imagesPath = "images";
export const INPUT_TITLE = 'Kafka Tools';

type DarkLightPath = { light: string; dark: string}

Expand Down Expand Up @@ -36,7 +37,7 @@ export class Icons {

public static get Trash(): DarkLightPath {
return getDarkLightPath("trashcan.svg");
}
}

public static get Warning(): string {
return getIconPath("warning.svg");
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function activate(context: vscode.ExtensionContext): void {
context.subscriptions.push(new SelectedClusterStatusBarItem(clusterSettings));

// Commands
const createTopicCommandHandler = new CreateTopicCommandHandler(clientAccessor, explorer);
const createTopicCommandHandler = new CreateTopicCommandHandler(clientAccessor, clusterSettings, explorer);
const deleteTopicCommandHandler = new DeleteTopicCommandHandler(clientAccessor, explorer);
const produceRecordCommandHandler = new ProduceRecordCommandHandler(clientAccessor, outputChannelProvider, explorer, workspaceSettings);
const startConsumerCommandHandler = new StartConsumerCommandHandler(clientAccessor, clusterSettings, consumerCollection, explorer);
Expand Down
133 changes: 133 additions & 0 deletions src/wizards/clusters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { QuickPickItem, window } from "vscode";
import { SaslOption } from "../client";
import { INPUT_TITLE } from "../constants";
import { KafkaExplorer } from "../explorer/kafkaExplorer";
import { ClusterSettings } from "../settings/clusters";
import { MultiStepInput, showErrorMessage, State } from "./multiStepInput";
import { validateBroker, validateClusterName, validateAuthentificationUserName } from "./validators";

const DEFAULT_BROKER = 'localhost:9092';

interface AddClusterState extends State {
bootstrap: string;
name: string;
saslOption: SaslOption;
}

export async function addClusterWizard(clusterSettings: ClusterSettings, explorer: KafkaExplorer): Promise<void> {

const state: Partial<AddClusterState> = {
totalSteps: 3
};

async function collectInputs(state: Partial<AddClusterState>, clusterSettings: ClusterSettings) {
await MultiStepInput.run(input => inputBrokers(input, state, clusterSettings));
}

async function inputBrokers(input: MultiStepInput, state: Partial<AddClusterState>, clusterSettings: ClusterSettings) {
state.bootstrap = await input.showInputBox({
title: INPUT_TITLE,
step: input.getStepNumber(),
totalSteps: state.totalSteps,
value: state.bootstrap ? state.bootstrap : DEFAULT_BROKER,
prompt: 'Broker(s) (localhost:9092,localhost:9093...)',
validate: validateBroker
});
return (input: MultiStepInput) => inputClusterName(input, state, clusterSettings);
}

async function inputClusterName(input: MultiStepInput, state: Partial<AddClusterState>, clusterSettings: ClusterSettings) {
const existingClusterNames = clusterSettings.getAll().map(cluster => cluster.name);
state.name = await input.showInputBox({
title: INPUT_TITLE,
step: input.getStepNumber(),
totalSteps: state.totalSteps,
value: state.name || '',
prompt: 'Friendly name',
validationContext: existingClusterNames,
validate: validateClusterName
});

return (input: MultiStepInput) => inputAuthentification(input, state);
}

async function inputAuthentification(input: MultiStepInput, state: Partial<AddClusterState>) {
const authOptions: QuickPickItem[] = [{ "label": "None" }, { "label": "SASL/PLAIN" }];
const authentification = (await input.showQuickPick({
title: INPUT_TITLE,
step: input.getStepNumber(),
totalSteps: state.totalSteps,
placeholder: 'Pick authentification',
items: authOptions,
activeItem: authOptions[0]
})).label;
if (authentification && authentification == authOptions[1].label) {
state.saslOption = { mechanism: 'plain' };
return (input: MultiStepInput) => inputAuthentificationUserName(input, state);
}
return undefined;
}

async function inputAuthentificationUserName(input: MultiStepInput, state: Partial<AddClusterState>) {
if (!state.saslOption) {
return;
}
state.saslOption.username = await input.showInputBox({
title: INPUT_TITLE,
step: input.getStepNumber(),
totalSteps: state.totalSteps,
value: state.saslOption?.username || '',
prompt: ' Username',
validate: validateAuthentificationUserName
});

return (input: MultiStepInput) => inputAuthentificationPassword(input, state);
}

async function inputAuthentificationPassword(input: MultiStepInput, state: Partial<AddClusterState>) {
if (!state.saslOption) {
return;
}
state.saslOption.password = await input.showInputBox({
title: INPUT_TITLE,
step: input.getStepNumber(),
totalSteps: state.totalSteps,
value: state.saslOption.password || '',
prompt: ' Password'
});
}

try {
await collectInputs(state, clusterSettings);
} catch (e) {
showErrorMessage('Error while collecting inputs for creating cluster', e);
return;
}

const addClusterState: AddClusterState = state as AddClusterState;
const bootstrap = state.bootstrap;
if (!bootstrap) {
return;
}
const name = state.name;
if (!name) {
return;
}
const saslOption = addClusterState.saslOption;
const sanitizedName = name.replace(/[^a-zA-Z0-9]/g, "");
const suffix = Buffer.from(bootstrap).toString("base64").replace(/=/g, "");

try {
clusterSettings.upsert({
id: `${sanitizedName}-${suffix}`,
bootstrap,
name,
saslOption,
});
explorer.refresh();
window.showInformationMessage(`Cluster '${name}' created successfully`);
}
catch (error) {
showErrorMessage(`Error while creating cluster`, error);
}
}
Loading

0 comments on commit 3c8ef1e

Please sign in to comment.