Skip to content

Commit

Permalink
Feature: VariableMenu component (#1294)
Browse files Browse the repository at this point in the history
* Add variable menu (broken currently)

* Fix access check

* Add an update event

* Don't use delteItem utility

* Fix comment

* Don't inconsistnetly use template checks

* Fix emit location

* Feature: Variable edit modal (#1295)

* Update locale strings

* Improve validation handler

* Add comment for TODO

* Add new components to bucket export

* Add VariableEditModal to VariableMenu

* Add missing export

* Emit update and create events

* Remove non-null assertion in edit modal - it's not needed

* Re-emit the new edit menu emission (Variable)
  • Loading branch information
znicholasbrown authored Mar 31, 2023
1 parent 0ad62f9 commit 54d7ffc
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 4 deletions.
10 changes: 6 additions & 4 deletions src/components/VariableCreateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@

<template #actions>
<p-button :loading="pending" @click="submit">
Create
{{ localization.info.create }}
</p-button>
</template>
<template #cancel>
<p-button inset @click="internalValue = false">
Close
{{ localization.info.close }}
</p-button>
</template>
</p-modal>
Expand All @@ -36,7 +36,7 @@
import { computed, ref } from 'vue'
import { useWorkspaceApi } from '@/compositions'
import { localization } from '@/localization'
import { VariableCreate } from '@/models'
import { Variable, VariableCreate } from '@/models'
import { isRequired, isString } from '@/utilities'
const props = defineProps<{
Expand All @@ -45,6 +45,7 @@
const emit = defineEmits<{
(event: 'update:showModal', value: boolean): void,
(event: 'create', value: Variable): void,
}>()
const internalValue = computed({
Expand Down Expand Up @@ -102,10 +103,11 @@
tags: tags.value,
}
await api.variables.createVariable(values)
const variable = await api.variables.createVariable(values)
showToast(localization.success.createVariable, 'success')
internalValue.value = false
emit('create', variable)
} catch (error) {
console.error(error)
showToast(localization.error.createVariable, 'error')
Expand Down
124 changes: 124 additions & 0 deletions src/components/VariableEditModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<template>
<p-modal v-model:showModal="internalValue" :title="localization.info.newVariable">
<p-form @submit="submit">
<p-content>
<p-label :label="localization.info.name" :state="nameState" :message="nameErrorMessage">
<p-text-input v-model="name" :state="nameState" />
</p-label>

<p-label :label="localization.info.value" :state="valueState" :message="valueErrorMessage">
<p-text-input v-model="value" :state="valueState" />
</p-label>

<p-label :label="localization.info.tags">
<p-tag-input v-model="tags" />
</p-label>
</p-content>
</p-form>

<template #actions>
<p-button :loading="pending" @click="submit">
{{ localization.info.save }}
</p-button>
</template>
<template #cancel>
<p-button inset @click="internalValue = false">
{{ localization.info.close }}
</p-button>
</template>
</p-modal>
</template>

<script lang="ts" setup>
import { showToast } from '@prefecthq/prefect-design'
import { useValidation, useValidationObserver, ValidationRule } from '@prefecthq/vue-compositions'
import { isNull } from 'lodash'
import { computed, ref } from 'vue'
import { useWorkspaceApi } from '@/compositions'
import { localization } from '@/localization'
import { Variable, VariableEdit } from '@/models'
import { isRequired, isString } from '@/utilities'
const props = defineProps<{
variable: Variable,
showModal: boolean,
}>()
const emit = defineEmits<{
(event: 'update:showModal', value: boolean): void,
(event: 'update', value: Variable): void,
}>()
const internalValue = computed({
get() {
return props.showModal
},
set(value: boolean): void {
emit('update:showModal', value)
},
})
const api = useWorkspaceApi()
const isUnique: ValidationRule<string | undefined> = async (value, label, { signal, source, previousValue }) => {
if (value === previousValue) {
return
}
if (source === 'validator') {
await new Promise((resolve) => setTimeout(resolve, 1000))
}
if (signal.aborted) {
return
}
if (isNull(value) || !isString(value)) {
return false
}
try {
const variable = await api.variables.getVariableByName(value)
return variable.id === props.variable.id
} catch {
/* Variable doesn't exist: silence is golden */
return true
}
}
const { validate, pending } = useValidationObserver()
const name = ref<string>(props.variable.name)
const value = ref<string>(props.variable.value)
const tags = ref<string[]>(props.variable.tags)
const rules: Record<string, ValidationRule<string | undefined>[]> = {
name: [isRequired(localization.info.name), isUnique],
value: [isRequired(localization.info.value)],
}
const { error: nameErrorMessage, state: nameState } = useValidation(name, localization.info.name, rules.name)
const { error: valueErrorMessage, state: valueState } = useValidation(value, localization.info.value, rules.value)
const submit = async (): Promise<void> => {
const valid = await validate()
if (valid) {
try {
const values: VariableEdit = {
name: name.value,
value: value.value,
tags: tags.value,
}
const variable = await api.variables.editVariable(props.variable.id, values)
showToast(localization.success.editVariable, 'success')
internalValue.value = false
emit('update', variable)
} catch (error) {
console.error(error)
showToast(localization.error.editVariable, 'error')
}
}
}
</script>
67 changes: 67 additions & 0 deletions src/components/VariableMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<p-icon-button-menu v-bind="$attrs">
<copy-overflow-menu-item :label="localization.info.copyId" :item="variable.id" />
<copy-overflow-menu-item :label="localization.info.copyName" :item="variable.name" />
<copy-overflow-menu-item :label="localization.info.copyValue" :item="variable.value" />
<p-overflow-menu-item v-if="can.update.variable" :label="localization.info.edit" @click="openEditModal" />
<p-overflow-menu-item v-if="can.delete.variable" :label="localization.info.delete" @click="openDeleteModal" />
</p-icon-button-menu>

<VariableEditModal v-model:showModal="showEditModal" :variable="variable" @update="handleUpdate" />

<ConfirmDeleteModal
v-model:showModal="showDeleteModal"
:label="localization.info.delete"
:name="variable.name"
@delete="deleteVariable(variable.id)"
/>
</template>

<script lang="ts">
export default {
name: 'VariableMenu',
expose: [],
inheritAttrs: false,
}
</script>

<script lang="ts" setup>
import { showToast } from '@prefecthq/prefect-design'
import { ConfirmDeleteModal, CopyOverflowMenuItem, VariableEditModal } from '@/components'
import { useWorkspaceApi, useCan, useShowModal } from '@/compositions'
import { localization } from '@/localization'
import { Variable } from '@/models'
defineProps<{
variable: Variable,
}>()
const emit = defineEmits<{
(event: 'delete', value: string): void,
(event: 'update', value: Variable): void,
}>()
const can = useCan()
const { showModal: showDeleteModal, open: openDeleteModal, close: closeDeleteModal } = useShowModal()
const { showModal: showEditModal, open: openEditModal } = useShowModal()
const api = useWorkspaceApi()
const deleteVariable = async (id: string): Promise<void> => {
closeDeleteModal()
try {
await api.variables.deleteVariable(id)
showToast(localization.success.delete(localization.info.variable), 'success')
emit('delete', id)
} catch (error) {
console.error(error)
showToast(localization.error.delete(localization.info.variable.toLowerCase()), 'error')
}
}
const handleUpdate = (variable: Variable): void => {
emit('update', variable)
}
</script>
4 changes: 4 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export { default as PageHeadingNotificationCreate } from './PageHeadingNotificat
export { default as PageHeadingNotificationEdit } from './PageHeadingNotificationEdit.vue'
export { default as PageHeadingNotifications } from './PageHeadingNotifications.vue'
export { default as PageHeadingTaskRun } from './PageHeadingTaskRun.vue'
export { default as PageHeadingVariables } from './PageHeadingVariables.vue'
export { default as PageHeadingWorkPool } from './PageHeadingWorkPool.vue'
export { default as PageHeadingWorkPoolCreate } from './PageHeadingWorkPoolCreate.vue'
export { default as PageHeadingWorkPoolEdit } from './PageHeadingWorkPoolEdit.vue'
Expand Down Expand Up @@ -222,6 +223,9 @@ export { default as TaskRunLogs } from './TaskRunLogs.vue'
export { default as TaskRunsSort } from './TaskRunsSort.vue'
export { default as TimezoneSelect } from './TimezoneSelect.vue'
export { default as ToastFlowRunCreate } from './ToastFlowRunCreate.vue'
export { default as VariableMenu } from './VariableMenu.vue'
export { default as VariableCreateModal } from './VariableCreateModal.vue'
export { default as VariableEditModal } from './VariableEditModal.vue'
export { default as ViewModeButtonGroup } from './ViewModeButtonGroup.vue'
export { default as WorkersLateIndicator } from './WorkersLateIndicator.vue'
export { default as WorkersTable } from './WorkersTable.vue'
Expand Down
11 changes: 11 additions & 0 deletions src/localization/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const en = {
createWorkQueue: 'Failed to create work queue',
delete: (type: string) => `Failed to delete ${type}`,
deleteSavedSearch: 'Failed to delete saved filter',
editVariable: 'Failed to updated variable',
pauseDeployment: 'Failed to pause deployment',
pauseFlowRun: 'Failed to pause flow run',
pauseNotification: 'Failed to pause notification',
Expand Down Expand Up @@ -73,6 +74,7 @@ export const en = {
createWorkQueue: 'Work queue created',
delete: (type: string) => `${type} deleted`,
deleteSavedSearch: 'Saved filter deleted',
editVariable: 'Variable updated',
pauseDeployment: 'Deployment paused',
pauseFlowRun: 'Flow run paused',
pauseNotification: 'Notification paused',
Expand All @@ -98,18 +100,27 @@ export const en = {
artifactTypeChanged: (type: string) => `Changed to \`${type}\` artifact`,
newVariable: 'New variable',
editVariable: (name: string) => `Edit \`${name}\``,
close: 'Close',
save: 'Save',
name: 'Name',
value: 'Value',
latest: 'Latest',
item: 'Item',
noData: 'No data',
copyId: 'Copy ID',
copyName: 'Copy name',
copyValue: 'Copy value',
edit: 'Edit',
delete: 'Delete',
tags: 'Tags',
invalidData: (docsUrl: string) => `Invalid data, see [documentation](${docsUrl}) for more information`,
noResults: 'No tracked results, enable [result persistence](https://docs.prefect.io/concepts/results/#persisting-results) to track results.',
flowRun: 'Flow run',
taskRun: 'Task run',
taskRuns: 'Task runs',
variable: 'Variable',
created: 'Created',
create: 'Create',
lastUpdated: 'Last Updated',
deprecatedWorkQueue: 'This work queue uses a deprecated tag-based approach to matching flow runs; it will continue to work but you can\'t modify it',
deploymentMissingWorkQueue: 'This deployment doesn\'t have an associated work queue; runs will be scheduled but won\'t be picked up by your agents',
Expand Down
1 change: 1 addition & 0 deletions src/services/can.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const workspacePermissions = [
'delete:work_pool',
'delete:workspace_bot_access',
'delete:workspace_user_access',
'delete:variable',
'read:automation',
'read:block',
'read:concurrency_limit',
Expand Down

0 comments on commit 54d7ffc

Please sign in to comment.