Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Scale Rules Feature #166

Merged
merged 38 commits into from
Aug 3, 2022
Merged

Add Scale Rules Feature #166

merged 38 commits into from
Aug 3, 2022

Conversation

MicroFish91
Copy link
Contributor

Feature support for adding scale rules. Currently only supporting HTTP and Azure Queue rules, no custom.

image

@MicroFish91 MicroFish91 requested a review from a team as a code owner July 21, 2022 23:30
package.nls.json Outdated
@@ -26,5 +26,6 @@
"containerApps.deleteConfirmation.EnterName": "Prompts with an input box where you enter the Container Apps environment name to delete.",
"containerApps.deleteConfirmation.ClickButton": "Prompts with a warning dialog where you click a button to delete.",
"containerApps.openConsoleInPortal": "Open Console in Portal",
"containerApps.editScalingRange": "Edit Scale Rule Setting..."
"containerApps.editScalingRange": "Edit Scale Range",
Copy link
Member

Choose a reason for hiding this comment

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

Usually any command that has follow up prompts ends with .... The idea is, something like Start App is one-click, and the action executes. Something like Edit Scale Range... has more prompts that the user must enter before the action occurs.

@@ -167,6 +167,11 @@
"command": "containerApps.editScalingRange",
"title": "%containerApps.editScalingRange%",
"category": "Azure Container Apps"
},
{
"command": "containerApps.addScaleRule",
Copy link
Member

Choose a reason for hiding this comment

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

Should also be added in "activationEvents" array in the package.json.

import { updateContainerApp } from "../../updateContainerApp";
import { IAddScaleRuleWizardContext } from "./IAddScaleRuleWizardContext";

export class AddNewScaleRule extends AzureWizardExecuteStep<IAddScaleRuleWizardContext> {
Copy link
Member

Choose a reason for hiding this comment

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

nit: Usually we include Step in the class name. Also, I think Add and New are somewhat redundant, so maybe just name it AddScaleRuleStep?

const node: ScaleRuleGroupTreeItem = nonNullProp(context, "treeItem");
const containerApp: ContainerAppTreeItem = node.parent.parent instanceof RevisionTreeItem ? node.parent.parent.parent.parent : node.parent.parent;

const adding = localize('addingScaleRule', 'Adding scale rule setting to "{0}"...', containerApp.name);
Copy link
Member

Choose a reason for hiding this comment

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

May be helpful to include the type of scale rule being added.

if (idx !== -1) {
scaleRules[idx] = scaleRule;
} else {
scaleRules.push(scaleRule);
Copy link
Member

Choose a reason for hiding this comment

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

I think it makes sense to just have either a default case that always pushes the scaleRule in and instead of replacing the HTTP rule by overwriting scaleRules[ind], to just splice it out. That way you could do something more like

switch (context.ruleType) {
            case ScaleRuleTypes.HTTP:
                const idx: number = scaleRules.findIndex((rule) => rule.http);
                if (idx) {
                    scaleRules.splice(idx, 1)
                }
            case ScaleRuleTypes.Queue:
            default:
                   scaleRules.push(scaleRule);
}

import { GetQueueLengthStep } from './queue/GetQueueLengthStep';
import { GetQueueNameStep } from './queue/GetQueueNameStep';

export class GetScaleRuleTypeStep extends AzureWizardPromptStep<IAddScaleRuleWizardContext> {
Copy link
Member

Choose a reason for hiding this comment

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

I think we can just call it ScaleRuleTypeStep. I think a promptStep is kind of assumed to be a "get" of some sort.

node = await ext.tree.showTreeItemPicker<ScaleRuleGroupTreeItem>(new RegExp(ScaleRuleGroupTreeItem.contextValue), context);
}

const title: string = localize('addScaleRuleTitle', 'Create Scale Rule');
Copy link
Member

Choose a reason for hiding this comment

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

Kind of weird that it's called "Add Scale Rule" everywhere, but the title is "Create Scale Rule".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I actually consciously made that choice just because it felt like a more accurate description of those specific steps, but I totally see why the lack of consistency would feel weird so I'll change it back

import { updateContainerApp } from "../../updateContainerApp";
import { IAddScaleRuleWizardContext } from "./IAddScaleRuleWizardContext";

export class AddNewScaleRule extends AzureWizardExecuteStep<IAddScaleRuleWizardContext> {
Copy link
Member

Choose a reason for hiding this comment

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

Some of this logic could be moved to ScaleRuleGroupTreeItem.createChildImpl. If you look at ManagedEnvironmentTreeItem, you'll see what I mean, but the benefit of doing this is that it'll automatically handle adding the child to the parent in the UI. You will also see the "Creating..." node under the parent as it's creating.

If you need more context/have questions, let's talk about it offline.

if (!/^[1-9]+[0-9]*$/.test(length)) {
return localize('invalidQueueLength', 'The number of requests must be a whole number greater than or equal to 1.');
}
if (Number(length) > thirtyTwoBitMaxSafeInteger) {
Copy link
Member

Choose a reason for hiding this comment

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

You can probably leave thirtyTwoBitMaxSafeInteger in this file since it's only being used in this one spot.

ruleType?: string;
concurrentRequests?: string;
queueName?: string;
queueLength?: string;
Copy link
Member

Choose a reason for hiding this comment

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

It's a matter of preference, but I usually define the context values to match the "true" value is going to be at the time of the API call. What I mean is, queueLength will come in as a string because of user input, but when being sent in the request, it should be a number.

Therefore, I would do the conversion in the prompt step rather than at the time of the API call. The benefit being, if we ever use that step again for something else, we wouldn't have to remember to convert it to a number.

package.json Outdated
},
{
"command": "containerApps.addScaleRule",
"when": "view == containerApps && viewItem =~ /scaleRules(?![a-z])/i",
Copy link
Member

Choose a reason for hiding this comment

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

Somewhat of a nit, but this part of the regex (?![a-z]) is unnecessary. The reason the command above requires it because there are scale, scaleRule and scaleRules context values, so if just /scale/ is matched, it matches for all 3 of these. However, scaleRules will only match to scaleRules.

I don't necessarily feel strongly about keeping it this way because it could be kind of future proofing, but just thought you should understand why it was there in the first place.

public priority: number = 100;

public async execute(context: IAddScaleRuleWizardContext, _progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
const adding = localize('addingScaleRule', 'Adding "{0}" {1} type rule to "{2}"...', context.ruleName, context.ruleType, context.containerApp.name);
Copy link
Member

Choose a reason for hiding this comment

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

The wording of this seems kind of awkward to me:

"Adding "httpRule" HTTP scaling type rule to "app1"..."

Maybe?

Adding HTTP scaling rule "httpRule" to "app1"...

void window.showInformationMessage(added);
ext.outputChannel.appendLog(added);
context.scaleRule = scaleRule;
} catch (error) {
Copy link
Member

Choose a reason for hiding this comment

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

Have you tried just throwing this error? Our callWithTelemetryAndErrorHandling should handle a thrown error by displaying an error window and output message. Plus, not including the error and just saying that it failed seems less useful to the user since the error usually contains information as to why it failed.

You actually shouldn't even need to wrap this in a try/catch at all and just let our error handling throw it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh I see, I should have checked how createChildImpl was called, it looks like it just swallows the error without displaying it which covers the issue I was trying to avoid by doing this (having two errors pop up because of it not being percolated up properly)

scaleRuleGroup: ScaleRuleGroupTreeItem;
ruleName?: string;
ruleType?: string;
concurrentRequests?: string;
Copy link
Member

Choose a reason for hiding this comment

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

It might be cleaner to group the properties that are specific to ruleTypes into their own objects. For example:

httpProperties: {
  concurrentRequests?: string;
},
queueProperties?: {
  queueName?: string;
  queueLength?: string;
  secretRef?: string;
  triggerParameter?: string;
}

length = length ? length.trim() : '';

const thirtyTwoBitMaxSafeInteger = 2147483647;
if (!/^[1-9]+[0-9]*$/.test(length)) {
Copy link
Member

Choose a reason for hiding this comment

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

Could probably make this regex a constant.

Or if HttpConcurrentRequestStep actually follows thirtyTwoBitMaxSafeInteger as well, maybe you could make a base step that has this implementation of validateInput

context.showCreatingTreeItem(nonNullProp(wizardContext, 'ruleName'));
await wizard.execute();

if (wizardContext.error !== undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

You should be able to just throw the error from the wizard step instead of tracking it and then throwing it after the execution.

Copy link
Member

@nturinski nturinski left a comment

Choose a reason for hiding this comment

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

Shoot, I think my suggestion may have introduced a bug in the back button. I can explain offline, but it's because you're using the httpProperties/queueProperties in the context now 😬


public async prompt(context: IAddScaleRuleWizardContext): Promise<void> {
const qpItems: QuickPickItem[] = [];
for (const ruleType in ScaleRuleTypes) { qpItems.push({ label: ScaleRuleTypes[ruleType as keyof typeof ScaleRuleTypes] }); }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for (const ruleType in ScaleRuleTypes) { qpItems.push({ label: ScaleRuleTypes[ruleType as keyof typeof ScaleRuleTypes] }); }
const qpItems: QuickPickItem[] = Object.values(ScaleRuleTypes).map(type => { return { label: type } });

const secrets: Secret[] | undefined = containerAppWithSecrets.configuration.secrets;
const qpItems: QuickPickItem[] = secrets?.map((secret) => {
return { label: nonNullProp(secret, "name") };
}) || [];
Copy link
Member

Choose a reason for hiding this comment

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

You might want to push a quickpick in here that says something like that they need a secretRef or maybe just throw an error if it's empty with an error saying to create a secretRef in the Portal?

const qpItems: QuickPickItem[] = secrets?.map((secret) => {
return { label: nonNullProp(secret, "name") };
}) || [];
context.queueProps.secretRef = (await context.ui.showQuickPick(qpItems, {})).label;
Copy link
Member

Choose a reason for hiding this comment

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

No placeHolder text?

return rule?.name?.length && rule?.name === name;
});
if (scaleRuleExists) {
return localize('scaleRuleExists', 'The scale rule "{0}" already exists in container app "{1}". Please enter a unique name.', name, this.containerApp?.name as string);
Copy link
Member

Choose a reason for hiding this comment

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

nit: I don't think you need the as string typing at the end since localize doesn't care if it's undefined or not.


export class QueueAuthSecretStep extends AzureWizardPromptStep<IAddScaleRuleWizardContext> {
public async prompt(context: IAddScaleRuleWizardContext): Promise<void> {
const noSecrets: string = localize('noSecretsFound', 'No secrets were found. Create a secret to proceed.');
Copy link
Member

Choose a reason for hiding this comment

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

nit: I personally prefer to initializing variables closer to where they are being used.


ext.outputChannel.appendLog(adding);
await updateContainerApp(context, context.containerApp, { template });
context.scaleRule = scaleRule;
Copy link
Member

Choose a reason for hiding this comment

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

nit: I don't think that there's really any point in doing this. You're not really using it anywhere else, as far as I can tell.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah good catch, this had a specific use in a previous iteration, but now has gone the way of the appendix 😂

Edit: Just kidding, I'm still using it here

image

Copy link
Member

Choose a reason for hiding this comment

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

Whoops

Copy link
Member

@nturinski nturinski left a comment

Choose a reason for hiding this comment

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

In general, anything I write with "nit:" means stylistically, it's something I felt like I could comment on, but it's not something that I would actually block you merging your PR in for (especially if you disagree)

}
currentNode = currentNode.parent;
}
return foundParent ? currentNode as T : null;
Copy link
Member

Choose a reason for hiding this comment

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

I think you should throw some sort of error here if you don't get a parent. If it returns as null, you'll get some funky errors in other parts of code. That way you won't have to do any as T typings either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, this looks way cleaner now

@MicroFish91
Copy link
Contributor Author

Okay, I think I fixed all relevant change suggestions, please review again when ready :)

}
const containerApp: ContainerAppTreeItem = treeUtils.findNearestParent(node, ContainerAppTreeItem.prototype);
await node.createChild(context);
await containerApp?.refresh(context);
Copy link
Member

Choose a reason for hiding this comment

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

nit: Probably don't need the ? anymore

}
if (!foundParent) {
const notFound: string = localize('parentNotFound', 'Could not find nearest parent "{0}".', parentInstance);
throw Error(notFound);
Copy link
Member

Choose a reason for hiding this comment

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

You have this somewhere else, but in general, we do throw new Error(). I didn't want to make you change it without a good reason, but I found this https://www.geeksforgeeks.org/difference-between-throw-errormsg-and-throw-new-errormsg/ and think it makes sense to keep using new based off some of the benefits.

Copy link
Member

@nturinski nturinski left a comment

Choose a reason for hiding this comment

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

🎉🎉🎉~~Congrats on your first big feature PR! 53 total comments isn't too bad ~~🎉🎉🎉

DanceDancingGIF

@MicroFish91 MicroFish91 merged commit aeaad95 into main Aug 3, 2022
@MicroFish91 MicroFish91 deleted the mwf/feat-add-scale-ruleset branch August 3, 2022 20:30
@microsoft microsoft locked and limited conversation to collaborators Mar 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants