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

Dvc 8568 variations update in variables create #309

Merged
merged 3 commits into from
Oct 6, 2023
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ $ npm install -g @devcycle/cli
$ dvc COMMAND
running command...
$ dvc (--version)
@devcycle/cli/5.11.1 linux-x64 node-v18.16.0
Copy link
Contributor

Choose a reason for hiding this comment

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

are we supposed to be committing these changes? my env always generates them but i just delete them

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Im not sure lol, Its just in the readme so I dont think it particularly matters

@devcycle/cli/5.11.1 darwin-x64 node-v16.20.0
$ dvc --help [COMMAND]
USAGE
$ dvc COMMAND
Expand Down
4 changes: 3 additions & 1 deletion docs/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Create a new Variable for an existing Feature.
USAGE
$ dvc variables create [--config-path <value>] [--auth-path <value>] [--repo-config-path <value>] [--client-id
<value>] [--client-secret <value>] [--project <value>] [--no-api] [--headless] [--key <value>] [--name <value>]
[--type String|Boolean|Number|JSON] [--feature <value>] [--description <value>]
[--type String|Boolean|Number|JSON] [--feature <value>] [--variations <value>] [--description <value>]

FLAGS
--description=<value> Description for the dashboard
Expand All @@ -26,6 +26,8 @@ FLAGS
--name=<value> Human readable name
--type=<option> The type of variable
<options: String|Boolean|Number|JSON>
--variations=<value> Set a value for this variable in each variation of the associated feature. Should be a JSON
object with the keys being variation keys.

GLOBAL FLAGS
--auth-path=<value> Override the default location to look for an auth.yml file
Expand Down
8 changes: 7 additions & 1 deletion oclif.manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "5.11.0",
"version": "5.11.1",
"commands": {
"authCommand": {
"id": "authCommand",
Expand Down Expand Up @@ -4731,6 +4731,12 @@
"description": "The key or id of the feature to create the variable for",
"multiple": false
},
"variations": {
"name": "variations",
"type": "option",
"description": "Set a value for this variable in each variation of the associated feature. Should be a JSON object with the keys being variation keys.",
"multiple": false
},
"description": {
"name": "description",
"type": "option",
Expand Down
4 changes: 2 additions & 2 deletions src/api/zodClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,9 @@ const Feature = z.object({
_createdBy: z.string().optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
variations: z.array(Variation).optional(),
variations: z.array(Variation),
controlVariation: z.string(),
variables: z.array(Variable).optional(),
variables: z.array(Variable),
tags: z.array(z.string()).optional(),
ldLink: z.string().optional(),
readonly: z.boolean(),
Expand Down
80 changes: 59 additions & 21 deletions src/commands/variables/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,43 @@ describe('variables create', () => {
'createdAt': '2023-06-14T18:56:21.270Z',
'updatedAt': '2023-06-14T18:56:21.270Z'
}
const mockFeature = {
'key': 'spam',
'variables': [
{
'_feature': featureId,
'key': mockVariable.key,
'type': mockVariable.type,
'name': mockVariable.name
}
],
'variations': [
{
'key': 'variation-on',
'name': 'Variation On',
'variables': {
'spam': 'ayyy',
}
},
{
'key': 'variation-off',
'name': 'Variation Off',
'variables': {
'spam': 'lmao',
}
}
]
}
// Headless mode
dvcTest()
.nock(BASE_URL, (api) => api
.post(`/v1/projects/${projectKey}/variables`, {
...requestBody,
_feature: featureId
})
.post(`/v1/projects/${projectKey}/variables`, requestBody)
.reply(200, mockVariable)
)
.stdout()
.command([
'variables create',
'--project', projectKey,
'--feature', featureId,
'--type', requestBody.type,
'--name', requestBody.name,
'--key', requestBody.key,
Expand All @@ -49,20 +72,19 @@ describe('variables create', () => {
(ctx) => {
expect(JSON.parse(ctx.stdout)).to.eql(mockVariable)
})

dvcTest()
.stderr()
.command([
'variables create',
'--project', projectKey,
'--feature', featureId,
'--type', requestBody.type,
'--name', requestBody.name,
'--headless',
...authFlags
])
.it('Errors when called in headless mode with no key', (ctx) => {
expect(ctx.stderr).to.contain('The key, name, feature, and type flags are required')
expect(ctx.stderr).to.contain('The key, name, and type flags are required')
})

dvcTest()
Expand All @@ -88,31 +110,47 @@ describe('variables create', () => {

dvcTest()
.nock(BASE_URL, (api) => api
.post(`/v1/projects/${projectKey}/variables`, {
...requestBody,
_feature: featureId
.get(`/v1/projects/${projectKey}/features/${featureId}`)
.reply(200, {
key: mockVariable.key,
_id: featureId,
variations: [
{
'_id': '64b05702053947be0d079ef3',
'key': 'variation-on',
'name': 'Variation On',
'variables': {}
},
{
'_id': '64b05702053947be0d079ef4',
'key': 'variation-off',
'name': 'Variation Off',
'variables': {}
}
],
variables: []
})
.reply(200, mockVariable)
.patch(`/v1/projects/${projectKey}/features/spam`, mockFeature)
.reply(200, mockFeature)
)
.stub(inquirer, 'prompt', () => {
return {
key: requestBody.key,
description: undefined,
associateToFeature: true,
_feature: featureId
}
})
.stdout()
.command([
'variables create',
'--project', projectKey,
'--type', requestBody.type,
'--key', requestBody.key,
'--name', requestBody.name,
'--feature', featureId,
'--variations', '{"variation-on": "ayyy","variation-off": "lmao"}',
'--headless',
...authFlags
])
.it('includes _feature in request when user selects to attach to a feature',
(ctx) => {
expect(JSON.parse(ctx.stdout)).to.eql(mockVariable)
expect(ctx.stdout).to.contain(
// eslint-disable-next-line max-len
'The variable was associated to the existing feature spam. Use "dvc features get --keys=spam" to see its details'
)
})

})
70 changes: 60 additions & 10 deletions src/commands/variables/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
createVariable,
} from '../../api/variables'
import CreateCommand from '../createCommand'
import { createVariablePrompts, variableFeaturePrompt } from '../../ui/prompts'
import { CreateVariableDto } from '../../api/schemas'
import { createVariablePrompts, featurePrompt } from '../../ui/prompts'
import { CreateVariableDto, Variable } from '../../api/schemas'
import inquirer from '../../ui/autocomplete'
import { VariableListOptions } from '../../ui/prompts/listPrompts/variablesListPrompt'
import { fetchFeatureByKey, updateFeature } from '../../api/features'

export default class CreateVariable extends CreateCommand {
static hidden = false
Expand All @@ -20,6 +22,10 @@ export default class CreateVariable extends CreateCommand {
'feature': Flags.string({
description: 'The key or id of the feature to create the variable for'
}),
'variations': Flags.string({
description: 'Set a value for this variable in each variation of the associated feature. ' +
'Should be a JSON object with the keys being variation keys.'
}),
'description': Flags.string({
description: 'Description for the dashboard',
}),
Expand All @@ -29,29 +35,73 @@ export default class CreateVariable extends CreateCommand {

public async run(): Promise<void> {
const { flags } = await this.parse(CreateVariable)
const { key, name, type, feature, headless, project } = flags
const { key, name, type, feature, headless, project, variations } = flags
await this.requireProject(project, headless)

if (headless && (!key || !name || !type || !feature)) {
this.writer.showError('The key, name, feature, and type flags are required')
if (headless && (!key || !name || !type)) {
this.writer.showError('The key, name, and type flags are required')
return
}
flags._feature = feature
if (headless && variations && !flags._feature) {
this.writer.showError('Cannot modify variations without associating to a feature')
return
}

let params = await this.populateParametersWithZod(CreateVariableDto, this.prompts, flags)
const params = await this.populateParametersWithZod(CreateVariableDto, this.prompts, flags)
if (!headless) {
const { associateToFeature } = await inquirer.prompt([{
name: 'associateToFeature',
message: 'Would you like to associate this variable to a feature?',
type: 'confirm',
default: false
}])

if (associateToFeature) {
params = await this.populateParametersWithZod(CreateVariableDto, [variableFeaturePrompt], params)
const { feature } = await inquirer.prompt([featurePrompt], {
token: this.authToken,
projectKey: this.projectKey
})
if (!feature) {
this.writer.showError(`Feature with key ${feature.key} could not be found`)
return
}
feature.variables.push(params)
const variableListOptions = new VariableListOptions(
[params as Variable],
this.writer,
!variations ? feature.variations: undefined // don't prompt for variations if flag provided
)
await variableListOptions.promptVariationValues(params as Variable)
await updateFeature(this.authToken, this.projectKey, feature.key, feature)
const message = `The variable was associated to the existing feature ${feature.key}. ` +
`Use "dvc features get --keys=${feature.key}" to see its details`
this.writer.successMessage(message)
} else {
const result = await createVariable(this.authToken, this.projectKey, params)
this.writer.showResults(result)
}
} else {
if (flags._feature) {
const feature = await fetchFeatureByKey(this.authToken, this.projectKey, flags._feature)
if (!feature) {
this.writer.showError(`Feature with key ${flags._feature} could not be found`)
return
}
const parsedVariations = JSON.parse(variations as string)
feature.variables.push(params as Variable)
for (const vari of feature.variations) {
vari.variables = vari.variables || {}
vari.variables[params.key] = parsedVariations[vari.key]
}
await updateFeature(this.authToken, this.projectKey, feature.key, feature)
const message = `The variable was associated to the existing feature ${feature.key}. ` +
`Use "dvc features get --keys=${feature.key}" to see its details.`
this.writer.showRawResults(message)
} else {
const result = await createVariable(this.authToken, this.projectKey, params)
this.writer.showResults(result)
}
}
const result = await createVariable(this.authToken, this.projectKey, params)
this.writer.showResults(result)
}
}
24 changes: 12 additions & 12 deletions src/ui/prompts/listPrompts/variablesListPrompt.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { createVariablePrompts, getVariableValuePrompt } from '../variablePrompts'
import { ListOption, ListOptionsPrompt } from './listOptionsPrompt'
import inquirer from 'inquirer'
import {
CreateVariableDto,
CreateVariableParams,
UpdateVariableDto,
import {
CreateVariableDto,
CreateVariableParams,
UpdateVariableDto,
Variable,
Variation
Variation
} from '../../../api/schemas'
import { errorMap } from '../../../api/apiClient'
import Writer from '../../writer'
Expand All @@ -27,8 +27,8 @@ export class VariableListOptions extends ListOptionsPrompt<CreateVariableParams>
}

getVariablesListPrompt = () => ({
name: 'variables',
value: 'variables',
name: 'variables',
value: 'variables',
message: 'Manage variables',
type: 'listOptions',
listOptionsPrompt: () => this.prompt(),
Expand All @@ -37,12 +37,12 @@ export class VariableListOptions extends ListOptionsPrompt<CreateVariableParams>
)
})

private async promptVariationValues(variable: Variable) {
async promptVariationValues(variable: Variable) {
if (this.featureVariations?.length) {
for (const variation of this.featureVariations) {
const variationPrompt = getVariableValuePrompt(
variation,
variable.type,
variation,
variable.type,
variation.variables?.[variable.key] as string | number | boolean
)

Expand Down Expand Up @@ -78,7 +78,7 @@ export class VariableListOptions extends ListOptionsPrompt<CreateVariableParams>
type: 'list',
choices: list
}])
const index = list.findIndex((listItem) => (listItem.value.item.key === variableListItem.item.key))
const index = list.findIndex((listItem) => (listItem.value.item.key === variableListItem.item.key))

// Have a default for each of the prompts that correspond to the previous value of the variable
const filledOutPrompts = this.variablePropertyPrompts.map((prompt) => ({
Expand All @@ -101,7 +101,7 @@ export class VariableListOptions extends ListOptionsPrompt<CreateVariableParams>
transformToListOptions(list: CreateVariableParams[]): ListOption<CreateVariableParams>[] {
return list.map((createVariable, index) => ({
name: createVariable.key,
value: { item: createVariable, id: index }
value: { item: createVariable, id: index }
}))
}
}
Loading