-
Notifications
You must be signed in to change notification settings - Fork 12
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
Conversation
…inerapps into mwf/feat-add-scale-ruleset
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", |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
c0aa434
to
f7f69a3
Compare
package.json
Outdated
}, | ||
{ | ||
"command": "containerApps.addScaleRule", | ||
"when": "view == containerApps && viewItem =~ /scaleRules(?![a-z])/i", |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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
src/tree/ScaleRuleGroupTreeItem.ts
Outdated
context.showCreatingTreeItem(nonNullProp(wizardContext, 'ruleName')); | ||
await wizard.execute(); | ||
|
||
if (wizardContext.error !== undefined) { |
There was a problem hiding this comment.
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.
There was a problem hiding this 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] }); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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") }; | ||
}) || []; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No placeHolder text?
22ff8f9
to
3c3818e
Compare
3c3818e
to
a40cd1e
Compare
…inerapps into mwf/feat-add-scale-ruleset
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); |
There was a problem hiding this comment.
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.'); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops
There was a problem hiding this 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)
src/utils/treeUtils.ts
Outdated
} | ||
currentNode = currentNode.parent; | ||
} | ||
return foundParent ? currentNode as T : null; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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); |
There was a problem hiding this comment.
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
src/utils/treeUtils.ts
Outdated
} | ||
if (!foundParent) { | ||
const notFound: string = localize('parentNotFound', 'Could not find nearest parent "{0}".', parentInstance); | ||
throw Error(notFound); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feature support for adding scale rules. Currently only supporting
HTTP
andAzure Queue
rules, no custom.