Skip to content

Commit

Permalink
fix(plugins-form): general fixes [khcp-9513] (#934)
Browse files Browse the repository at this point in the history
Misc. fixes for unification plugin forms for [KHCP-9513](https://konghq.atlassian.net/browse/KHCP-9513).
  • Loading branch information
kaiarrowood authored Nov 22, 2023
1 parent b7c3b6f commit 7cc69bd
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@
import { computed, ref, reactive, provide, watch, type PropType, onBeforeMount, defineComponent } from 'vue'
import type { AxiosResponse, AxiosRequestConfig } from 'axios'
import {
EntityTypeIdField,
type PluginScope,
type PluginEntityInfo,
type KonnectPluginFormConfig,
type KongManagerPluginFormConfig,
} from '../types'
Expand Down Expand Up @@ -84,13 +83,9 @@ const props = defineProps({
/**
* Entity data if plugin is scoped
*/
entityId: {
type: String,
default: '',
},
entityType: {
type: String as PropType<PluginScope>,
default: null,
entityMap: {
type: Object as PropType<Record<string, PluginEntityInfo>>,
default: () => ({}),
},
/**
* Plugin data if being edited
Expand Down Expand Up @@ -126,7 +121,7 @@ const { axiosInstance } = useAxios({
headers: props.config.requestHeaders,
})
const { parseSchema } = composables.useSchemas(props.entityId || undefined)
const { parseSchema } = composables.useSchemas(props.entityMap.focusedEntity?.id || undefined)
const { convertToDotNotation, unFlattenObject, isObjectEmpty, unsetNullForeignKey } = composables.usePluginHelpers()
// define endpoints for use by KFG
Expand Down Expand Up @@ -221,22 +216,6 @@ const originalModel = reactive<Record<string, any>>({})
const formModel = reactive<Record<string, any>>({})
const formOptions = computed(() => form.value?.options)
// ex. service_id will be converted to service-id
const entityIdField = computed((): string => {
switch (props.entityType) {
case 'service':
return EntityTypeIdField.SERVICE
case 'route':
return EntityTypeIdField.ROUTE
case 'consumer':
return EntityTypeIdField.CONSUMER
case 'consumer_group':
return EntityTypeIdField.CONSUMER_GROUP
default:
return ''
}
})
// This function transforms the form data into the correct structure to be submitted to the API
const getModel = (): Record<string, any> => {
const schema = { ...props.schema }
Expand Down Expand Up @@ -520,18 +499,25 @@ const initFormModel = (): void => {
...(props.record.tags && { tags: props.record.tags }),
})
if (props.record.data) {
updateModel(props.record.data)
} else if (props.record.config) {
// scope and top level fields
// handle credentials
if (props.credential) {
// scope
if (props.record.consumer_id || props.record.consumer) {
updateModel({
consumer_id: props.record.consumer_id || props.record.consumer,
})
}
updateModel(props.record)
} else if (props.record.config) { // typical plugins
// scope fields
if ((props.record.consumer_id || props.record.consumer) || (props.record.service_id || props.record.service) ||
(props.record.route_id || props.record.route) || (props.record.consumer_group_id || props.record.consumer_group)) {
updateModel({
service_id: props.record.service_id || props.record.service,
route_id: props.record.route_id || props.record.route,
consumer_id: props.record.consumer_id || props.record.consumer,
consumer_group_id: props.record.consumer_group_id || props.record.consumer_group,
enabled: props.record.enabled,
})
}
Expand All @@ -542,26 +528,29 @@ const initFormModel = (): void => {
// scoping logic, convert _ to - for form model
// Check if incoming field exists in current model and if so update
if (entityIdField.value && props.entityId && props.schema) {
if (Object.keys(props.entityMap).length && !props.entityMap.global && props.schema) {
const updateFields: Record<string, any> = {}
const key = entityIdField.value === 'consumer_group_id' ? 'consumer_group-id' : JSON.parse(JSON.stringify(entityIdField.value).replace('_', '-'))
// ex. set consumer-id: <entityId>
if (Object.prototype.hasOwnProperty.call(formModel, key)) {
updateFields[key] = props.entityId
for (const entity in props.entityMap) {
const id = props.entityMap[entity].id
const idField = props.entityMap[entity].idField
const key = idField === 'consumer_group_id' ? 'consumer_group-id' : JSON.parse(JSON.stringify(idField).replace('_', '-'))
// ex. set consumer-id: <entityId>
if (Object.prototype.hasOwnProperty.call(formModel, key)) {
updateFields[key] = id
}
}
updateModel(updateFields)
}
// Check if entity field exists in current model and if so update
// credentials don't recognize field with -id, so for now set it like
// ex. consumer: <entityId>
// we'll fix this on submit
if (props.entityId && props.schema && props.credential) {
if (props.entityMap.consumer?.id && props.schema && props.credential) {
const updateFields: Record<string, any> = {}
if (Object.prototype.hasOwnProperty.call(formModel, props.entityType)) {
updateFields[props.entityType] = props.entityId
if (Object.prototype.hasOwnProperty.call(formModel, 'consumer')) {
updateFields.consumer = props.entityMap.consumer.id
}
updateModel(updateFields)
Expand Down
183 changes: 117 additions & 66 deletions packages/entities/entities-plugins/src/components/PluginForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,46 +51,49 @@
:config="config"
:credential="treatAsCredential"
:editing="formType === EntityBaseFormType.Edit"
:entity-id="entityData.id"
:entity-type="entityData.entity"
:entity-map="entityMap"
:record="record || undefined"
:schema="schema ?? {}"
:schema="schema || {}"
@loading="(val: boolean) => formLoading = val"
@model-updated="handleUpdate"
/>

<template
v-if="!isWizardStep"
#form-actions
>
<KButton
appearance="outline"
data-testid="form-cancel"
:disabled="form.isReadonly"
type="reset"
@click="handleClickCancel"
>
{{ t('actions.cancel') }}
</KButton>
<KButton
v-if="formType === EntityBaseFormType.Create && config.backRoute"
appearance="secondary"
class="form-back-button"
data-testid="form-back"
:disabled="form.isReadonly"
@click="handleClickBack"
>
{{ t('actions.back') }}
</KButton>
<KButton
appearance="primary"
data-testid="form-submit"
:disabled="!canSubmit || form.isReadonly"
type="submit"
@click="saveFormData"
>
{{ t('actions.save') }}
</KButton>
<template #form-actions>
<!--
Force the render of this slot
- if isWizardStep is true we don't want any buttons displayed (default EntityBaseForm buttons included)
-->
<div v-if="isWizardStep" />
<div v-else>
<KButton
appearance="secondary"
data-testid="form-cancel"
:disabled="form.isReadonly"
type="reset"
@click="handleClickCancel"
>
{{ t('actions.cancel') }}
</KButton>
<KButton
v-if="formType === EntityBaseFormType.Create && config.backRoute"
appearance="secondary"
class="form-back-button"
data-testid="form-back"
:disabled="form.isReadonly"
@click="handleClickBack"
>
{{ t('actions.back') }}
</KButton>
<KButton
appearance="primary"
data-testid="form-submit"
:disabled="!canSubmit || form.isReadonly"
type="submit"
@click="saveFormData"
>
{{ t('actions.save') }}
</KButton>
</div>
</template>
</EntityBaseForm>
</div>
Expand All @@ -104,6 +107,7 @@ import { marked, type MarkedOptions } from 'marked'
import { useAxios, useErrors, useHelpers, useStringHelpers, EntityBaseForm, EntityBaseFormType } from '@kong-ui-public/entities-shared'
import '@kong-ui-public/entities-shared/dist/style.css'
import {
EntityTypeIdField,
PluginScope,
type KonnectPluginFormConfig,
type KongManagerPluginFormConfig,
Expand Down Expand Up @@ -200,7 +204,6 @@ const { axiosInstance } = useAxios({
headers: props.config.requestHeaders,
})
const fetchUrl = computed((): string => endpoints.form[props.config.app].edit)
const formType = computed((): EntityBaseFormType => props.pluginId ? EntityBaseFormType.Edit : EntityBaseFormType.Create)
const schema = ref<Record<string, any> | null>(null)
const treatAsCredential = computed((): boolean => !!(props.credential && props.config.entityId))
Expand All @@ -223,6 +226,20 @@ const form = reactive<PluginFormState>({
errorMessage: '',
})
const fetchUrl = computed((): string => {
if (treatAsCredential.value) { // credential
let submitEndpoint = endpoints.form[props.config.app].credential[formType.value]
// replace resource endpoint for credentials
submitEndpoint = submitEndpoint.replace(/{resourceEndpoint}/gi, resourceEndpoint.value)
return submitEndpoint
}
// plugin
return endpoints.form[props.config.app].edit
})
// non-editable plugin type. They shouldn't be able to get to this unless they manually
// type in the URL
const isDisabled = computed((): boolean => {
Expand All @@ -231,37 +248,72 @@ const isDisabled = computed((): boolean => {
return currentPlugin ? (customSchemas[currentPlugin as keyof typeof customSchemas] as Record<string, any>)?.configurationDisabled : false
})
const entityData = computed((): PluginEntityInfo => {
const entityMap = computed((): Record<string, PluginEntityInfo> => {
const consumerId = (props.config.entityType === 'consumers' && props.config.entityId) || record.value?.consumer?.id
const consumerGroupId = (props.config.entityType === 'consumer_groups' && props.config.entityId) || record.value?.consumer_group?.id
const serviceId = (props.config.entityType === 'services' && props.config.entityId) || record.value?.service?.id
const routeId = (props.config.entityType === 'routes' && props.config.entityId) || record.value?.route?.id
let entity = PluginScope.GLOBAL
let endpoint = props.config.entityType || 'plugins'
// global plugins
if (!(consumerId || consumerGroupId || serviceId || routeId)) {
return {
global: {
entity: PluginScope.GLOBAL,
entityEndpoint: 'plugins',
},
}
}
const entityMap: Record<string, PluginEntityInfo> = {}
// the order of these if statements is important
// they should match the order used to define entityIdField in
// PluginEntityForm.vue
// scoped plugins
if (serviceId) {
entity = PluginScope.SERVICE
endpoint = 'services'
} else if (routeId) {
entity = PluginScope.ROUTE
endpoint = 'routes'
} else if (consumerId) {
entity = PluginScope.CONSUMER
endpoint = 'consumers'
} else if (consumerGroupId) {
entity = PluginScope.CONSUMER_GROUP
endpoint = 'consumer_groups'
entityMap.service = {
entity: PluginScope.SERVICE,
entityEndpoint: 'services',
id: serviceId,
idField: EntityTypeIdField.SERVICE,
}
}
return {
entity,
entityEndpoint: endpoint,
id: consumerId || consumerGroupId || serviceId || routeId,
if (routeId) {
entityMap.route = {
entity: PluginScope.ROUTE,
entityEndpoint: 'routes',
id: routeId,
idField: EntityTypeIdField.ROUTE,
}
}
if (consumerId) {
entityMap.consumer = {
entity: PluginScope.CONSUMER,
entityEndpoint: 'consumers',
id: consumerId,
idField: EntityTypeIdField.CONSUMER,
}
}
if (consumerGroupId) {
entityMap.consumer_group = {
entity: PluginScope.CONSUMER_GROUP,
entityEndpoint: 'consumer_groups',
id: consumerGroupId,
idField: EntityTypeIdField.CONSUMER_GROUP,
}
}
// the actual entity requested in the config from the host app
if (props.config.entityType) {
entityMap.focusedEntity = {
entity: PluginScope[props.config.entityType.substring(0, props.config.entityType.length - 1).toUpperCase() as keyof typeof EntityTypeIdField],
entityEndpoint: props.config.entityType,
id: props.config.entityId,
idField: EntityTypeIdField[props.config.entityType.substring(0, props.config.entityType.length - 1).toUpperCase() as keyof typeof EntityTypeIdField],
}
}
return entityMap
})
// Configuration for globally shared fields
Expand Down Expand Up @@ -333,12 +385,9 @@ const defaultFormSchema: DefaultPluginsSchemaRecord = reactive({
// This is specifically used for credential plugins
// To create an 'ACL' credential we will end up submitting to a URL like: /<entityType>/<entityId>/acl
const resourceEndpoint = computed((): string => {
const entityPath: EntityType = entityData.value.entityEndpoint
const entityPath: EntityType = 'consumers'
let type = '/plugins'
if (treatAsCredential.value) {
type = credentialMetaData[props.pluginType]?.endpoint
}
const type = credentialMetaData[props.pluginType]?.endpoint || '/plugins'
return `${entityPath}/${props.config.entityId}${type}`
})
Expand Down Expand Up @@ -714,19 +763,19 @@ const handleUpdate = (payload: Record<string, any>) => {
}
}
watch([entityData, initialized], (newData, oldData) => {
const newId = newData[0] !== oldData[0]
watch([entityMap, initialized], (newData, oldData) => {
const newEntityData = newData[0] !== oldData[0]
const newinitialized = newData[1]
const oldinitialized = oldData[1]
// rebuild schema if its not a credential and we either just determined a new entity id, or newly initialized the data
if (!treatAsCredential.value && formType.value === EntityBaseFormType.Edit && (newId || (newinitialized && newinitialized !== oldinitialized))) {
if (!treatAsCredential.value && formType.value === EntityBaseFormType.Edit && (newEntityData || (newinitialized && newinitialized !== oldinitialized))) {
schemaLoading.value = true
schema.value = buildFormSchema('config', configResponse.value, defaultFormSchema)
schemaLoading.value = false
}
})
}, { deep: true })
/**
* ---------------
Expand Down Expand Up @@ -814,6 +863,8 @@ const saveFormData = async (): Promise<void> => {
requestBody[entityKey] = { id: props.config.entityId }
}
}
delete requestBody.created_at
}
// TODO: determine validate URL for credentials
Expand Down
Loading

0 comments on commit 7cc69bd

Please sign in to comment.