Skip to content

Commit

Permalink
Feature: VariablesTable, VariablesDelete, filters, bulk and count…
Browse files Browse the repository at this point in the history
… routes (#1296)

* Add variabels delete button for bulk delete

* Add Variable filter and sort models; use them in the table (no route yet)

* Add filter and count routes, filter mappers

* Don't need to use a computed for the variables delete message

* Add pagination scheme
  • Loading branch information
znicholasbrown authored Mar 31, 2023
1 parent 54d7ffc commit 379d50d
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 11 deletions.
45 changes: 45 additions & 0 deletions src/components/VariablesDeleteButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<p-button v-if="variableIds.length > 0" danger icon="TrashIcon" @click="open" />
<ConfirmDeleteModal
v-model:showModal="showModal"
:name="localization.info.selectedVariables"
:label="localization.info.variables"
@delete="deleteVariables(variableIds)"
/>
</template>

<script lang="ts" setup>
import { showToast } from '@prefecthq/prefect-design'
import { computed } from 'vue'
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue'
import { useShowModal, useWorkspaceApi } from '@/compositions'
import { localization } from '@/localization'
import { toPluralString } from '@/utilities'
defineProps<{
variableIds: string[],
}>()
const emit = defineEmits<{
(event: 'delete'): string[],
}>()
const { showModal, open, close } = useShowModal()
const api = useWorkspaceApi()
const deleteVariables = async (variableIds: string[]): Promise<void> => {
try {
const variableDeletePromises = variableIds.map(api.variables.deleteVariable)
await Promise.all(variableDeletePromises)
const successMessage = localization.success.delete(`${variableIds.length} ${toPluralString(localization.info.variable, variableIds.length)}`)
showToast(successMessage, 'success')
emit('delete')
} catch (error) {
showToast(localization.error.delete(localization.info.variables), 'error')
} finally {
close()
}
}
</script>
189 changes: 189 additions & 0 deletions src/components/VariablesTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<template>
<div class="variables-table">
<p-layout-table sticky>
<template #header-start>
<div class="variables-table__header-start">
<ResultsCount v-if="selectedVariables.length == 0" :label="localization.info.variable" :count="variablesCount" />
<SelectedCount v-else :count="selectedVariables.length" />

<FlowsDeleteButton v-if="can.delete.variable" :selected="selectedVariables" @delete="deleteVariables" />
</div>
</template>

<template #header-end>
<div class="variables-table__header-end">
<SearchInput v-model="variableLike" :placeholder="localization.info.variablesSearch" :label="localization.info.variablesSearch" />
<p-select v-model="filter.sort" :options="variableSortOptions" />
<p-tags-input v-model="filter.variables.tags.name" :empty-message="localization.info.tags" class="variables-table__tags" />
</div>
</template>

<p-table :data="variables" :columns="columns">
<template #selection-heading>
<p-checkbox v-model="model" @update:model-value="selectAllVariables" />
</template>

<template #selection="{ row }">
<p-checkbox v-model="selectedVariables" :value="row.id" />
</template>

<template #name="{ row }">
<span>{{ row.name }}</span>
</template>

<template #updated="{ row }">
{{ formatDateTimeNumeric(row.updated) }}
</template>

<template #action-heading>
<span />
</template>

<template #action="{ row }">
<div class="variables-table__action">
<VariableMenu :variable="row" size="xs" @delete="refresh" />
</div>
</template>

<template #empty-state>
<PEmptyResults>
<template #message>
{{ localization.info.noVariables }}
</template>
<template v-if="isCustomFilter" #actions>
<p-button size="sm" secondary @click="clear">
Clear Filters
</p-button>
</template>
</PEmptyResults>
</template>
</p-table>

<template #footer-end>
<p-pager v-if="variables.length" v-model:page="offset" :pages="variablesCount" />
</template>
</p-layout-table>
</div>
</template>

<script lang="ts" setup>
import { PTable, PEmptyResults, CheckboxModel } from '@prefecthq/prefect-design'
import { useDebouncedRef, useSubscription } from '@prefecthq/vue-compositions'
import { computed, ref } from 'vue'
import { FlowsDeleteButton, VariableMenu, ResultsCount, SearchInput, SelectedCount } from '@/components'
import { useCan, useVariablesFilter, useWorkspaceApi } from '@/compositions'
import { localization } from '@/localization'
import { VariablesFilter } from '@/models/Filters'
import { variableSortOptions } from '@/types'
import { formatDateTimeNumeric } from '@/utilities/dates'
const props = defineProps<{
filter?: VariablesFilter,
}>()
const api = useWorkspaceApi()
const can = useCan()
const variableLike = ref<string>()
const variableLikeDebounced = useDebouncedRef(variableLike, 1000)
const offset = ref(0)
const { filter, isCustomFilter, clear } = useVariablesFilter({
...props.filter,
variables: {
...props.filter?.variables,
nameLike: variableLikeDebounced,
valueLike: variableLikeDebounced,
},
offset,
})
const columns = [
{
label: 'selection',
width: '20px',
visible: can.delete.variable,
},
{
property: 'name',
label: 'Name',
width: '125px',
},
{
property: 'value',
label: 'Value',
width: '125px',
},
{
property: 'updated',
label: 'Updated',
width: '125px',
},
{
label: 'Action',
width: '42px',
},
]
const selectedVariables = ref<string[]>([])
const selectAllVariables = (allVariablesSelected: CheckboxModel): string[] => {
if (allVariablesSelected) {
return selectedVariables.value = [...variables.value.map(variable => variable.id)]
}
return selectedVariables.value = []
}
const model = computed({
get() {
return selectedVariables.value.length === variables.value.length
},
set(value: boolean) {
selectAllVariables(value)
},
})
const variablesSubscription = useSubscription(api.variables.getVariables, [filter])
const variables = computed(() => variablesSubscription.response ?? [])
const variablesCountSubscription = useSubscription(api.variables.getVariablesCount, [filter])
const variablesCount = computed(() => variablesCountSubscription.response)
function refresh(): void {
variablesSubscription.refresh()
variablesCountSubscription.refresh()
}
const emit = defineEmits<{
(event: 'delete'): void,
}>()
const deleteVariables = (): void => {
selectedVariables.value = []
refresh()
emit('delete')
}
</script>

<style>
.variables-table__header-start { @apply
grow
whitespace-nowrap
}
.variables-table__header-end { @apply
flex
flex-wrap
pl-2
ml-auto
shrink
gap-2
}
.variables-table__tags {
min-width: 128px;
}
.variables-table__action { @apply
text-right
}
</style>
8 changes: 6 additions & 2 deletions src/compositions/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { FlowRunSortValuesSortParam } from '@/formatters/FlowRunSortValuesSortPa
import { FlowSortValuesSortParam } from '@/formatters/FlowSortValuesSortParam'
import { OperatorRouteParam } from '@/formatters/OperatorRouteParam'
import { TaskRunSortValuesSortParam } from '@/formatters/TaskRunSortValuesSortParam'
import { BlockDocumentFilter, BlockDocumentsFilter, BlockSchemaFilter, BlockSchemasFilter, BlockTypeFilter, BlockTypesFilter, DeploymentFilter, DeploymentsFilter, FlowFilter, FlowRunFilter, FlowRunsFilter, FlowRunsHistoryFilter, FlowsFilter, StateFilter, TagFilter, TaskRunFilter, TaskRunsFilter, UnionFilter, UnionFilterSort, WorkPoolFilter, WorkPoolQueueFilter, WorkPoolsFilter } from '@/models/Filters'
import { defaultDeploymentSort, defaultFlowRunSort, defaultFlowSort, defaultTaskRunSort } from '@/types'
import { BlockDocumentFilter, BlockDocumentsFilter, BlockSchemaFilter, BlockSchemasFilter, BlockTypeFilter, BlockTypesFilter, DeploymentFilter, DeploymentsFilter, FlowFilter, FlowRunFilter, FlowRunsFilter, FlowRunsHistoryFilter, FlowsFilter, StateFilter, TagFilter, TaskRunFilter, TaskRunsFilter, UnionFilter, UnionFilterSort, VariablesFilter, WorkPoolFilter, WorkPoolQueueFilter, WorkPoolsFilter } from '@/models/Filters'
import { defaultDeploymentSort, defaultFlowRunSort, defaultFlowSort, defaultTaskRunSort, defaultVariableSort } from '@/types'
import { AnyRecord } from '@/types/any'
import { MaybeReactive } from '@/types/reactivity'
import { merge } from '@/utilities/object'
Expand Down Expand Up @@ -515,6 +515,10 @@ export function useDeploymentsFilter(defaultValue: MaybeReactive<DeploymentsFilt
return useUnionFilter<DeploymentsFilter>(defaultValue, defaultDeploymentSort)
}

export function useVariablesFilter(defaultValue: MaybeReactive<VariablesFilter> = {}): UseFilter<VariablesFilter> {
return useUnionFilter<VariablesFilter>(defaultValue, defaultVariableSort)
}

const unionFilterSchema: Omit<RouteQueryParamsSchema<UnionFilter>, 'sort'> = {
flows: flowFilterSchema,
flowRuns: flowRunFilterSchema,
Expand Down
4 changes: 4 additions & 0 deletions src/localization/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,20 @@ export const en = {
artifact: 'Artifact',
artifacts: 'Artifacts',
artifactSearch: 'Search artifacts',
variablesSearch: 'Search variables',
artifactCreated: (key: string) => `Created __${key}__`,
artifactTypeChanged: (type: string) => `Changed to \`${type}\` artifact`,
newVariable: 'New variable',
editVariable: (name: string) => `Edit \`${name}\``,
close: 'Close',
save: 'Save',
name: 'Name',
selectedVariables: 'Selected variables',
value: 'Value',
latest: 'Latest',
item: 'Item',
noData: 'No data',
noVariables: 'No variables',
copyId: 'Copy ID',
copyName: 'Copy name',
copyValue: 'Copy value',
Expand All @@ -119,6 +122,7 @@ export const en = {
taskRun: 'Task run',
taskRuns: 'Task runs',
variable: 'Variable',
variables: 'Variables',
created: 'Created',
create: 'Create',
lastUpdated: 'Last Updated',
Expand Down
28 changes: 26 additions & 2 deletions src/maps/filters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import { asArray } from '@prefecthq/prefect-design'
import { Any, Like, All, IsNull, OperatorRequest, TagFilterRequest, FlowFilterRequest, FlowRunFilterRequest, NotAny, StateFilterRequest, Before, After, TaskRunFilterRequest, Exists, DeploymentFilterRequest, Equals, FlowsFilterRequest, FlowRunsFilterRequest, TaskRunsFilterRequest, DeploymentsFilterRequest, BlockTypeFilterRequest, BlockSchemaFilterRequest, BlockDocumentFilterRequest, NotificationsFilterRequest, SavedSearchesFilterRequest, LogsFilterRequest, GreaterThan, LessThan, ConcurrencyLimitsFilterRequest, BlockTypesFilterRequest, BlockSchemasFilterRequest, BlockDocumentsFilterRequest, WorkQueuesFilterRequest, StartsWith, WorkPoolFilterRequest, WorkPoolsFilterRequest, WorkPoolQueueFilterRequest, FlowRunsHistoryFilterRequest, WorkPoolWorkersFilterRequest, WorkPoolQueuesFilterRequest, ArtifactsFilterRequest, ArtifactFilterRequest, NullableEquals, Latest } from '@/models/api/Filters'
import { FlowFilter, FlowRunFilter, Operation, StateFilter, TagFilter, TaskRunFilter, DeploymentFilter, FlowsFilter, FlowRunsFilter, TaskRunsFilter, DeploymentsFilter, BlockTypeFilter, BlockSchemaFilter, BlockDocumentFilter, NotificationsFilter, SavedSearchesFilter, LogsFilter, ConcurrencyLimitsFilter, BlockTypesFilter, BlockSchemasFilter, BlockDocumentsFilter, WorkQueuesFilter, WorkPoolFilter, WorkPoolsFilter, WorkPoolQueueFilter, FlowRunsHistoryFilter, WorkPoolWorkersFilter, WorkPoolQueuesFilter, ArtifactsFilter, ArtifactFilter } from '@/models/Filters'
import { Any, Like, All, IsNull, OperatorRequest, TagFilterRequest, FlowFilterRequest, FlowRunFilterRequest, NotAny, StateFilterRequest, Before, After, TaskRunFilterRequest, Exists, DeploymentFilterRequest, Equals, FlowsFilterRequest, FlowRunsFilterRequest, TaskRunsFilterRequest, DeploymentsFilterRequest, BlockTypeFilterRequest, BlockSchemaFilterRequest, BlockDocumentFilterRequest, NotificationsFilterRequest, SavedSearchesFilterRequest, LogsFilterRequest, GreaterThan, LessThan, ConcurrencyLimitsFilterRequest, BlockTypesFilterRequest, BlockSchemasFilterRequest, BlockDocumentsFilterRequest, WorkQueuesFilterRequest, StartsWith, WorkPoolFilterRequest, WorkPoolsFilterRequest, WorkPoolQueueFilterRequest, FlowRunsHistoryFilterRequest, WorkPoolWorkersFilterRequest, WorkPoolQueuesFilterRequest, ArtifactsFilterRequest, ArtifactFilterRequest, NullableEquals, Latest, VariablesFilterRequest, VariableFilterRequest } from '@/models/api/Filters'
import { FlowFilter, FlowRunFilter, Operation, StateFilter, TagFilter, TaskRunFilter, DeploymentFilter, FlowsFilter, FlowRunsFilter, TaskRunsFilter, DeploymentsFilter, BlockTypeFilter, BlockSchemaFilter, BlockDocumentFilter, NotificationsFilter, SavedSearchesFilter, LogsFilter, ConcurrencyLimitsFilter, BlockTypesFilter, BlockSchemasFilter, BlockDocumentsFilter, WorkQueuesFilter, WorkPoolFilter, WorkPoolsFilter, WorkPoolQueueFilter, FlowRunsHistoryFilter, WorkPoolWorkersFilter, WorkPoolQueuesFilter, ArtifactsFilter, ArtifactFilter, VariablesFilter, VariableFilter } from '@/models/Filters'
import { MapFunction } from '@/services'
import { removeEmptyObjects } from '@/utilities'

Expand Down Expand Up @@ -289,6 +289,30 @@ export const mapArtifactsFilter: MapFunction<ArtifactsFilter, ArtifactsFilterReq
}
}

export const mapVariableFilter: MapFunction<VariableFilter, VariableFilterRequest> = function(source) {
return {
id: toAny(source.id),
name: {
...toAny(source.name),
...toLike(source.nameLike),
},
value: {
...toAny(source.value),
...toLike(source.valueLike),
},
tags: this.map('TagFilter', source.tags, 'TagFilterRequest'),
}
}

export const mapVariablesFilter: MapFunction<VariablesFilter, VariablesFilterRequest> = function(source) {
return {
variables: this.map('VariableFilter', source.variables, 'VariableFilterRequest'),
sort: source.sort,
limit: source.limit,
offset: source.offset,
}
}

export const mapFlowsFilter: MapFunction<FlowsFilter, FlowsFilterRequest> = function(source) {
return removeEmptyObjects({
flows: this.map('FlowFilter', source.flows, 'FlowFilterRequest'),
Expand Down
4 changes: 3 additions & 1 deletion src/maps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { mapStringToDate, mapDateToString } from '@/maps/date'
import { mapDeploymentResponseToDeployment, mapDeploymentUpdateToDeploymentUpdateRequest, mapDeploymentFlowRunCreateToDeploymentFlowRunRequest } from '@/maps/deployment'
import { mapRunHistoryToDivergingBarChartItem } from '@/maps/divergingBarChartItem'
import { mapEmpiricalPolicyToEmpiricalPolicyResponse, mapEmpiricalPolicyResponseToEmpiricalPolicy, mapEmpiricalPolicyToEmpiricalPolicyRequest } from '@/maps/empiricalPolicy'
import { mapFlowFilter, mapDeploymentFilter, mapFlowRunFilter, mapStateFilter, mapFlowsFilter, mapDeploymentsFilter, mapFlowRunsFilter, mapTagFilter, mapTaskRunFilter, mapTaskRunsFilter, mapBlockDocumentFilter, mapBlockSchemaFilter, mapBlockTypeFilter, mapBlockDocumentsFilter, mapBlockSchemasFilter, mapBlockTypesFilter, mapWorkPoolsFilter, mapWorkPoolFilter, mapWorkPoolQueueFilter, mapFlowRunsHistoryFilter, mapLogsFilter, mapNotificationsFilter, mapSavedSearchesFilter, mapWorkQueuesFilter, mapWorkPoolWorkersFilter, mapWorkPoolQueuesFilter, mapArtifactFilter, mapArtifactsFilter } from '@/maps/filters'
import { mapFlowFilter, mapDeploymentFilter, mapFlowRunFilter, mapStateFilter, mapFlowsFilter, mapDeploymentsFilter, mapFlowRunsFilter, mapTagFilter, mapTaskRunFilter, mapTaskRunsFilter, mapBlockDocumentFilter, mapBlockSchemaFilter, mapBlockTypeFilter, mapBlockDocumentsFilter, mapBlockSchemasFilter, mapBlockTypesFilter, mapWorkPoolsFilter, mapWorkPoolFilter, mapWorkPoolQueueFilter, mapFlowRunsHistoryFilter, mapLogsFilter, mapNotificationsFilter, mapSavedSearchesFilter, mapWorkQueuesFilter, mapWorkPoolWorkersFilter, mapWorkPoolQueuesFilter, mapArtifactFilter, mapArtifactsFilter, mapVariablesFilter, mapVariableFilter } from '@/maps/filters'
import { mapFlowToFlowResponse, mapFlowResponseToFlow } from '@/maps/flow'
import { mapFlowRunResponseToFlowRun } from '@/maps/flowRun'
import { mapSavedSearchFilterToFlowRunFilters } from '@/maps/flowRunFilter'
Expand Down Expand Up @@ -140,6 +140,8 @@ export const maps = {
UiFlowRunHistoryResponse: { UiFlowRunHistory: mapUiFlowRunHistoryResponseToUiFlowRunHistory },
VariableCreate: { VariableCreateRequest: mapVariableCreateToVariableCreateRequest },
VariableEdit: { VariableEditRequest: mapVariableEditToVariableEditRequest },
VariableFilter: { VariableFilterRequest: mapVariableFilter },
VariablesFilter: { VariablesFilterRequest: mapVariablesFilter },
VariableResponse: { Variable: mapVariableResponseToVariable },
WorkerScheduledFlowRunResponse: { WorkerScheduledFlowRun: mapWorkerScheduledFlowRunResponseToWorkerScheduledFlowRun },
WorkerScheduledFlowRuns: { WorkerScheduledFlowRunsRequest: mapWorkerScheduledFlowRunsToWorkerScheduledFlowRunsRequest },
Expand Down
18 changes: 17 additions & 1 deletion src/models/Filters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArtifactSortValues, FlowSortValues, FlowRunSortValues, TaskRunSortValues, DeploymentSortValues, LogSortValues } from '@/types'
import { ArtifactSortValues, FlowSortValues, FlowRunSortValues, TaskRunSortValues, DeploymentSortValues, LogSortValues, VariableSortValues } from '@/types'

export type Operation = 'and' | 'or'

Expand Down Expand Up @@ -87,6 +87,22 @@ export type ArtifactsFilter = {
offset?: number,
}

export type VariableFilter = {
id?: string[],
name?: string[],
nameLike?: string,
value?: string[],
valueLike?: string,
tags?: TagFilter,
}

export type VariablesFilter = {
variables?: VariableFilter,
sort?: VariableSortValues,
limit?: number,
offset?: number,
}

export type DeploymentFilter = {
id?: string[],
isScheduleActive?: boolean,
Expand Down
Loading

0 comments on commit 379d50d

Please sign in to comment.