3
0

Admin integration gateway UX improvements

- add admin integration gateway UX improvements
 - add purge requests button to admin profiler
 - added purge profiler hits
This commit is contained in:
Jože Fortun
2023-01-03 17:33:39 +01:00
committed by Peter Grlica
parent 921d4988c7
commit b250b9329f
20 changed files with 806 additions and 317 deletions

View File

@@ -11,17 +11,15 @@
@hidden="onHidden"
>
<div
v-if="filter"
v-if="internalFilter"
class="card-body"
>
<c-filter-params
:filter="filter"
@update="onUpdate"
:filter="internalFilter"
/>
<b-form-checkbox
v-model="filter.enabled"
v-model="internalFilter.enabled"
data-test-id="checkbox-filter-enable"
@change="onUpdate"
>
{{ $t('filters.enabled') }}
</b-form-checkbox>
@@ -53,25 +51,65 @@ export default {
data () {
return {
updated: false,
filteredFields: [],
internalFilter: undefined,
}
},
watch: {
visible: {
handler (visible) {
if (visible) {
// Convert to FE structure
this.internalFilter = {
...this.filter,
params: this.filter.params.map(p => {
if (this.filter.ref === 'response') {
let value = p.value || {}
if (p.type === 'header') {
value = Object.entries(value).map(([name, v = []]) => {
return { name, expr: v.join('') }
})
} else if (p.type === 'input') {
value = { type: 'Any', expr: '', ...value }
}
this.$set(p, 'value', value)
}
return p
}),
}
}
},
},
},
methods: {
onSave () {
this.$emit('submit', { ...this.filter, updated: this.updated })
this.updated = false
// Convert to BE structure
const filter = {
...this.internalFilter,
params: this.internalFilter.params.map(p => {
if (this.filter.ref === 'response') {
if (p.type === 'header') {
p.value = p.value.reduce((obj, { name, expr = '' }) => {
return { ...obj, [name]: [expr] }
}, {})
}
}
return p
}),
}
this.$emit('submit', { ...filter, updated: true })
this.internalFilter = undefined
},
onHidden () {
this.$emit('reset')
},
onUpdate () {
this.updated = true
},
},
}
</script>

View File

@@ -12,6 +12,7 @@
>
<template slot="label">
{{ $t(`filters.labels.${param.label}`) }}
<template v-if="param.label === 'expr'">
<a
v-if="param.label === 'expr'"
@@ -24,12 +25,13 @@
</a>
</template>
</template>
<!-- TODO create multi field component-->
<b-form-checkbox
v-if="param.type === 'bool'"
v-model="param.value"
@change="onUpdate"
/>
<vue-select
v-else-if="param.label === 'workflow'"
v-model="param.value"
@@ -37,13 +39,12 @@
:reduce="wf => wf.workflowID"
:placeholder="$t('filters.placeholders.workflow')"
class="bg-white"
@input="onUpdate"
/>
<b-form-select
v-else-if="param.label === 'status'"
v-model="param.value"
:options="httpStatusOptions"
@change="onUpdate"
>
<template #first>
<b-form-select-option
@@ -53,12 +54,70 @@
</b-form-select-option>
</template>
</b-form-select>
<template v-else-if="filter.ref === 'response'">
<template v-if="param.type === 'input'">
<b-form-select
v-model="param.value.type"
:options="inputTypeOptions"
class="mb-2"
/>
<b-input-group>
<b-input-group-prepend>
<b-button variant="dark">
ƒ
</b-button>
</b-input-group-prepend>
<b-form-input
v-model="param.value.expr"
:placeholder="$t('filters.help.expression.example')"
/>
</b-input-group>
</template>
<template v-else>
<b-input-group
v-for="(header, hIndex) in param.value"
:key="`header-${hIndex}`"
class="mb-2"
>
<b-form-input
v-model="header.name"
:placeholder="$t('filters.labels.name')"
/>
<b-form-input
v-model="header.expr"
:placeholder="$t('filters.labels.value')"
/>
<b-input-group-append>
<b-button
variant="danger"
@click="param.value.splice(hIndex, 1)"
>
<font-awesome-icon
:icon="['far', 'trash-alt']"
/>
</b-button>
</b-input-group-append>
</b-input-group>
<b-button
variant="link"
class="text-decoration-none px-0"
@click="param.value.push({ name: '', expr: '' })"
>
+ {{ $t('filters.addHeader') }}
</b-button>
</template>
</template>
<template v-else>
<b-form-textarea
v-if="param.label === 'jsfunc'"
v-model="param.value"
max-rows="6"
@change="onUpdate"
/>
<b-input-group v-else>
<b-input-group-prepend
@@ -72,12 +131,10 @@
v-if="param.label === 'expr'"
v-model="param.value"
:placeholder="$t('filters.help.expression.example')"
@change="onUpdate"
/>
<b-form-input
v-else
v-model="param.value"
@change="onUpdate"
/>
</b-input-group>
</template>
@@ -115,6 +172,18 @@ export default {
{ value: 307, text: this.$t('filters.httpStatus.307') },
{ value: 308, text: this.$t('filters.httpStatus.308') },
],
inputTypeOptions: [
'String',
'Any',
'Array',
'KV',
'DateTime',
'Float',
'Integer',
'Reader',
'Vars',
],
}
},
@@ -136,11 +205,5 @@ export default {
})
}
},
methods: {
onUpdate () {
this.$emit('update')
},
},
}
</script>

View File

@@ -1,9 +1,10 @@
<template>
<b-card
data-test-id="card-filter-list"
class="apigw shadow-sm mt-3"
header-bg-variant="white"
footer-bg-variant="white"
body-class="p-0"
class="apigw shadow-sm mt-3"
>
<c-filter-modal
:visible="!!selectedFilter"
@@ -12,55 +13,53 @@
@reset="onReset"
/>
<b-form
@submit.prevent="$emit('submit', route)"
<b-tabs
data-test-id="filter-steps"
active-nav-item-class="active-tab bg-white"
class="border-0"
content-class="border-bottom"
>
<b-tabs
data-test-id="filter-steps"
active-nav-item-class="active-tab bg-white"
class="border-0 font-weight-bold"
content-class="border-bottom"
<b-tab
v-for="(step, index) in steps"
:key="index"
class="border-0"
:title="$t(`filters.step_title.${step}`)"
@click="onActivateTab(index)"
>
<b-tab
v-for="(step, index) in steps"
:key="index"
class="border-0"
:title="$t(`filters.step_title.${step}`)"
@click="onActivateTab(index)"
>
<b-row class="d-flex flex-column w-100 m-0">
<c-filters-dropdown
class="px-1 py-2"
:available-filters="getAvailableFiltersByStep"
:filters="getSelectedFiltersByStep"
@addFilter="onAddFilter"
/>
<c-filters-table
:filters="getSelectedFiltersByStep"
:step="index"
:fetching="fetching"
@filterSelect="onFilterSelect"
@removeFilter="onRemoveFilter"
@sortFilters="onSortFilters"
/>
</b-tab>
</b-tabs>
<c-filters-table
ref="filterTable"
:filters="getSelectedFiltersByStep"
:selected-row="step.selectedRow"
:step="index"
@filterSelect="onFilterSelect"
@removeFilter="onRemoveFilter"
@sortFilters="onSortFilters"
/>
</b-row>
</b-tab>
</b-tabs>
</b-form>
<template #header>
<h3 class="m-0">
{{ $t('filters.title') }}
</h3>
</template>
<c-submit-button
class="float-right mt-3"
:processing="processing"
:success="success"
:disabled="disabled"
@submit="$emit('submit')"
/>
<template #footer>
<div
class="d-flex justify-content-between"
>
<c-filters-dropdown
:available-filters="getAvailableFiltersByStep"
:filters="getSelectedFiltersByStep"
@addFilter="onAddFilter"
/>
<c-submit-button
:processing="processing"
:success="success"
:disabled="disabled"
@submit="$emit('submit')"
/>
</div>
</template>
</b-card>
</template>
<script>
@@ -83,6 +82,10 @@ export default {
CFiltersDropdown,
},
props: {
fetching: {
type: Boolean,
value: false,
},
processing: {
type: Boolean,
value: false,

View File

@@ -1,11 +1,12 @@
<template>
<div>
<b-table-simple
class="filter-table"
hover
class="mb-0"
>
<b-thead>
<b-thead head-variant="light">
<b-tr>
<b-th />
<b-th>{{ $t('filters.list.filters') }}</b-th>
<b-th>{{ $t('filters.list.status') }}</b-th>
<b-th />
@@ -13,51 +14,77 @@
</b-thead>
<draggable
v-if="!fetching"
v-model="sortableFilters"
:options="{ handle: '.handle' }"
tag="b-tbody"
>
<b-tr
v-for="(filter, index) in sortableFilters"
:key="index"
class="pointer"
:class="[selectedRow===index ? 'row-selected' : 'row-not-selected']"
@click.stop="onRowClick(filter,index)"
>
<b-td class="align-baseline">
<td
class="handle align-middle grab"
style="width: 1%"
>
<font-awesome-icon
:icon="['fas', 'bars']"
class="text-light"
/>
</td>
<b-td class="align-middle">
{{ filter.label }}
</b-td>
<b-td class="align-baseline">
<b-td class="align-middle">
{{ $t(`filters.${filter.enabled ? 'enabled' : 'disabled'}`) }}
</b-td>
<b-td class="text-right align-baseline">
<b-td class="text-right align-middle">
<b-button
variant="danger"
class="my-1"
size="sm"
@click.stop="onRemoveFilter(filter)"
variant="link"
@click.stop="onRowClick(filter, index)"
>
{{ $t('filters.list.remove') }}
<font-awesome-icon
:icon="['fas', 'pen']"
/>
</b-button>
<c-input-confirm
class="ml-1"
@confirmed="onRemoveFilter(filter)"
@click.stop
/>
</b-td>
</b-tr>
</draggable>
</b-table-simple>
<h6
v-if="!sortableFilters.length"
data-test-id="no-filters"
class="d-flex justify-content-center align-items-center mb-3"
<div
class="d-flex flex-column align-items-center justify-content-center h-100 overflow-hidden"
>
{{ $t('filters.list.noFilters') }}
</h6>
<b-spinner
v-if="fetching"
class="my-4"
/>
<p
v-else-if="!sortableFilters.length"
data-test-id="no-filters"
class="my-4"
>
{{ $t('filters.list.noFilters') }}
</p>
</div>
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: {
draggable,
},
props: {
filters: {
type: Array,
@@ -67,6 +94,10 @@ export default {
type: Number,
default: () => 0,
},
fetching: {
type: Boolean,
value: false,
},
},
data () {
@@ -75,6 +106,7 @@ export default {
selectedFilter: {},
}
},
computed: {
sortableFilters: {
get () {
@@ -86,6 +118,7 @@ export default {
},
},
},
methods: {
onAddFilter (filter) {
if (!this.filters.find(f => f.ref === filter.ref)) {
@@ -109,12 +142,3 @@ export default {
},
}
</script>
<style lang="scss">
.filter-table .row-selected{
background: #F3F3F5;
}
.cursor-default{
cursor: default;
}
</style>

View File

@@ -30,6 +30,7 @@
>
<label
label-for="endpoint"
class="mb-0"
>
{{ $t('endpoint') }}
</label>
@@ -68,6 +69,16 @@
/>
</b-form-group>
<b-form-group
v-if="route.meta"
:label="$t('description')"
label-cols="2"
>
<b-form-textarea
v-model="route.meta.description"
/>
</b-form-group>
<b-form-group
label-cols="2"
:class="{ 'mb-0': !route.routeID }"

View File

@@ -1,53 +1,18 @@
<template>
<b-container
class="py-3"
<b-card
class="shadow-sm"
body-class="p-0"
header-bg-variant="white"
footer-bg-variant="white"
>
<c-content-header
:title="$t('title')"
<template
#header
>
<span
class="text-nowrap"
>
<b-button
v-if="canCreate"
data-test-id="button-add"
variant="primary"
:to="{ name: 'system.apigw.new' }"
>
{{ $t('new') }}
</b-button>
<b-button
v-if="$Settings.get('apigw.profilerEnabled', false)"
data-test-id="button-profiler"
class="ml-2"
variant="info"
:to="{ name: 'system.apigw.profiler' }"
>
{{ $t('profiler') }}
</b-button>
<c-permissions-button
v-if="canGrant"
data-test-id="button-permissions"
resource="corteza::system:apigw-route/*"
button-variant="light"
class="ml-2"
>
<font-awesome-icon :icon="['fas', 'lock']" />
{{ $t('permissions') }}
</c-permissions-button>
</span>
<b-dropdown
v-if="false"
variant="link"
right
menu-class="shadow-sm"
:text="$t('export')"
>
<b-dropdown-item-button variant="link">
{{ $t('yaml') }}
</b-dropdown-item-button>
</b-dropdown>
</c-content-header>
<h3 class="m-0">
{{ $t('title') }}
</h3>
</template>
<c-resource-list
:primary-key="primaryKey"
:filter="filter"
@@ -66,9 +31,52 @@
prevPagination: $t('admin:general.pagination.prev'),
nextPagination: $t('admin:general.pagination.next'),
}"
class="h-100"
@search="filterList"
>
<template #header>
<b-button
v-if="canCreate"
data-test-id="button-add"
variant="primary"
:to="{ name: 'system.apigw.new' }"
>
{{ $t('new') }}
</b-button>
<b-button
v-if="$Settings.get('apigw.profiler.enabled', false)"
data-test-id="button-profiler"
class="ml-1"
variant="info"
:to="{ name: 'system.apigw.profiler' }"
>
{{ $t('profiler') }}
</b-button>
<c-permissions-button
v-if="canGrant"
data-test-id="button-permissions"
resource="corteza::system:apigw-route/*"
button-variant="light"
class="ml-1"
>
<font-awesome-icon :icon="['fas', 'lock']" />
{{ $t('permissions') }}
</c-permissions-button>
<b-dropdown
v-if="false"
variant="link"
right
menu-class="shadow-sm"
:text="$t('export')"
>
<b-dropdown-item-button variant="link">
{{ $t('yaml') }}
</b-dropdown-item-button>
</b-dropdown>
<c-resource-list-status-filter
v-model="filter.deleted"
data-test-id="filter-deleted-routes"
@@ -76,6 +84,7 @@
:excluded-label="$t('filterForm.excluded.label')"
:inclusive-label="$t('filterForm.inclusive.label')"
:exclusive-label="$t('filterForm.exclusive.label')"
class="mt-3"
@change="filterList"
/>
</template>
@@ -92,13 +101,13 @@
</b-button>
</template>
</c-resource-list>
</b-container>
</b-card>
</template>
<script>
import * as moment from 'moment'
import listHelpers from 'corteza-webapp-admin/src/mixins/listHelpers'
import { mapGetters } from 'vuex'
import listHelpers from 'corteza-webapp-admin/src/mixins/listHelpers'
import moment from 'moment'
import { components } from '@cortezaproject/corteza-vue'
const { CResourceList } = components
@@ -112,14 +121,12 @@ export default {
],
i18nOptions: {
namespaces: [ 'system.apigw' ],
namespaces: 'system.apigw',
keyPrefix: 'list',
},
data () {
return {
id: 'routes',
primaryKey: 'routeID',
editRoute: 'system.apigw.edit',

View File

@@ -0,0 +1,124 @@
<template>
<b-card
class="shadow-sm"
header-bg-variant="white"
footer-bg-variant="white"
>
<template #header>
<h3 class="m-0">
{{ $t('title') }}
</h3>
</template>
<b-form-row
@submit.prevent="$emit('submit', settings)"
>
<b-col
cols="12"
md="6"
class="mb-3 mb-md-0"
>
<b-form-group
:label="$t('profiler.label')"
label-class="text-primary"
class="mb-0"
>
<b-form-radio-group
v-model="profilerSetting"
:options="profilerOptions"
button-variant="outline-primary"
size="sm"
buttons
/>
</b-form-group>
</b-col>
<b-col
cols="12"
md="6"
>
<b-form-group
:label="$t('proxy.label')"
label-class="text-primary"
class="mb-0"
>
<b-form-checkbox
v-model="settings['apigw.proxy.follow-redirects']"
>
{{ $t('proxy.follow') }}
</b-form-checkbox>
</b-form-group>
</b-col>
</b-form-row>
<template #footer>
<c-submit-button
class="float-right"
:processing="processing"
:success="success"
@submit="$emit('submit', settings)"
/>
</template>
</b-card>
</template>
<script>
import CSubmitButton from 'corteza-webapp-admin/src/components/CSubmitButton'
export default {
name: 'CSettingsEditor',
i18nOptions: {
namespaces: 'system.apigw',
keyPrefix: 'settings',
},
components: {
CSubmitButton,
},
props: {
settings: {
type: Object,
required: true,
},
processing: {
type: Boolean,
value: false,
},
success: {
type: Boolean,
value: false,
},
},
data () {
return {
profilerOptions: [
{ value: 'disabled', text: 'Disabled' },
{ value: 'filter', text: 'Enabled as filter' },
{ value: 'global', text: 'Enabled for all routes' },
],
}
},
computed: {
profilerSetting: {
get () {
if (this.settings['apigw.profiler.enabled']) {
return this.settings['apigw.profiler.global'] ? 'global' : 'filter'
}
return 'disabled'
},
set (setting) {
this.settings['apigw.profiler.enabled'] = ['filter', 'global'].includes(setting)
this.settings['apigw.profiler.global'] = setting === 'global'
},
},
},
}
</script>

View File

@@ -2,7 +2,7 @@
<b-card
data-test-id="card-requests"
no-body
class="shadow-sm mb-3"
class="shadow-sm"
footer-class="d-flex align-items-center justify-content-center"
footer-bg-variant="white"
header-bg-variant="white"
@@ -13,22 +13,33 @@
</h3>
<div
class="d-flex align-items-center justify-content-end"
class="d-flex align-items-center justify-content-between"
>
<span
:class="{ 'loading': loading }"
<div>
<b-button
data-test-id="button-refresh"
variant="primary"
:disabled="loading"
@click="loadItems()"
>
{{ $t('general:label.refresh') }}
</b-button>
<span
class="ml-1"
:class="{ 'loading': loading }"
>
{{ autoRefreshLabel }}
</span>
</div>
<c-input-confirm
:disabled="!items.length"
:borderless="false"
variant="danger"
@confirmed="purgeRequests"
>
{{ autoRefreshLabel }}
</span>
<b-button
data-test-id="button-refresh"
variant="primary"
:disabled="loading"
class="ml-2"
@click="loadItems()"
>
{{ $t('general:label.refresh') }}
</b-button>
{{ $t('purge.this') }}
</c-input-confirm>
</div>
</template>
@@ -66,13 +77,13 @@
<template #cell(actions)="row">
<b-button
data-test-id="button-edit-route"
size="sm"
variant="link"
class="p-0"
:to="{ name: 'system.apigw.profiler.hit.list', params: { hitID: row.item.hitID } }"
>
<font-awesome-icon
:icon="['fas', 'pen']"
:icon="['fas', 'info-circle']"
class="text-primary"
/>
</b-button>
</template>
@@ -141,12 +152,12 @@ export default {
{
key: 'time_start',
sortable: true,
formatter: v => fmt.fullDateTime(v),
formatter: v => v ? fmt.fullDateTime(v) : '',
},
{
key: 'time_finish',
sortable: true,
formatter: v => fmt.fullDateTime(v),
formatter: v => v ? fmt.fullDateTime(v) : '',
},
{
key: 'http_method',
@@ -161,12 +172,13 @@ export default {
{
key: 'content_length',
sortable: true,
formatter: v => `${v || 0} bytes`,
class: 'text-right',
},
{
key: 'time_duration',
sortable: true,
formatter: v => `${v.toFixed(2)} ms`,
formatter: v => v ? `${v.toFixed(2)} ms` : '',
class: 'text-right',
},
{
@@ -231,7 +243,9 @@ export default {
this.totalItems = append ? this.totalItems + set.length : this.totalItems
return { filter, set }
}).finally(() => {
})
.catch(this.toastErrorHandler(this.$t('notification:gateway.profiler.purge.fetch')))
.finally(() => {
if (!append) {
this.filter.before = oldBeforeID
}
@@ -239,6 +253,16 @@ export default {
})
},
purgeRequests () {
const { routeID } = this.filter
this.$SystemAPI.apigwProfilerPurge({ routeID })
.then(() => {
this.loadItems()
this.toastSuccess(this.$t('notification:gateway.profiler.purge.success'))
})
.catch(this.toastErrorHandler(this.$t('notification:gateway.profiler.purge.error')))
},
resetItems (sorting = this.sorting) {
this.sorting = sorting
this.filter.before = ''

View File

@@ -1,6 +1,5 @@
<template>
<b-card
data-test-id="card-edit-authentication"
header-bg-variant="white"
footer-bg-variant="white"
class="shadow-sm"

View File

@@ -46,6 +46,7 @@ import {
faCloud,
faQuestion,
faStamp,
faInfoCircle,
} from '@fortawesome/free-solid-svg-icons'
import {
@@ -109,4 +110,5 @@ library.add(
faCloud,
faQuestion,
faStamp,
faInfoCircle,
)

View File

@@ -8,7 +8,7 @@ import { components } from '@cortezaproject/corteza-vue'
import { LMap, LTileLayer, LMarker } from 'vue2-leaflet'
import 'leaflet/dist/leaflet.css'
import { Icon } from 'leaflet'
const { CCorredorManualButtons, CPermissionsButton } = components
const { CCorredorManualButtons, CPermissionsButton, CInputConfirm } = components
Vue.use(PortalVue)
Vue.component('c-corredor-manual-buttons', CCorredorManualButtons)
@@ -16,6 +16,7 @@ Vue.component('c-permissions-button', CPermissionsButton)
Vue.component('font-awesome-icon', FontAwesomeIcon)
Vue.component('c-content-header', CContentHeader)
Vue.component('c-resource-list-status-filter', CResourceListStatusFilter)
Vue.component('c-input-confirm', CInputConfirm)
// Map things
Vue.component('l-map', LMap)

View File

@@ -36,6 +36,7 @@
<c-filters-stepper
v-if="routeID"
ref="stepper"
:fetching="stepper.fetching"
:processing="stepper.processing"
:success="stepper.success"
:filters.sync="filters"
@@ -91,6 +92,7 @@ export default {
success: false,
},
stepper: {
fetching: false,
processing: false,
success: false,
},
@@ -115,7 +117,7 @@ export default {
},
showProfiler () {
return this.$Settings.get('apigw.profilerEnabled', false) && (this.$Settings.get('apigw.profilerGlobal', false) || this.filters.some(({ ref, enabled = false }) => ref === 'profiler' && enabled))
return this.$Settings.get('apigw.profiler.enabled', false) && (this.$Settings.get('apigw.profiler.global', false) || this.filters.some(({ ref, enabled = false }) => ref === 'profiler' && enabled))
},
},
@@ -128,7 +130,6 @@ export default {
if (this.routeID) {
this.fetchSteps()
this.fetchRoute()
this.fetchAllAvailableFilters()
this.fetchFilters()
} else {
this.route = {
@@ -262,24 +263,29 @@ export default {
fetchFilters () {
this.incLoader()
this.stepper.fetching = true
this.$SystemAPI.apigwFilterList({ routeID: this.routeID })
.then(({ set = [] }) => {
this.setRouteFilters(set)
return this.setRouteFilters(set)
})
.catch(this.toastErrorHandler(this.$t('notification:gateway.filter.fetch.error')))
.finally(() => {
this.decLoader()
this.stepper.fetching = false
})
},
setRouteFilters (routeFilters = []) {
this.filters = (routeFilters || []).map(filter => {
const f = { ...this.availableFilters.find((af) => af.ref === filter.ref) }
f.params = this.decodeParams(f, { ...filter.params })
f.weight = parseInt(filter.weight)
f.filterID = filter.filterID
f.enabled = !!filter.enabled
return { ...f }
return this.fetchAllAvailableFilters().then(() => {
this.filters = (routeFilters || []).map(filter => {
const f = { ...this.availableFilters.find((af) => af.ref === filter.ref) }
f.params = this.decodeParams(f, { ...filter.params })
f.weight = parseInt(filter.weight)
f.filterID = filter.filterID
f.enabled = !!filter.enabled
return { ...f }
})
})
},
@@ -303,10 +309,11 @@ export default {
fetchAllAvailableFilters () {
this.incLoader()
this.$SystemAPI.apigwFilterDefFilter()
return this.$SystemAPI.apigwFilterDefFilter()
.then((api) => {
this.availableFilters = api.map((f) => {
return { name, ...f, ref: f.name, enabled: true, options: { checked: false } }
return { ...f, ref: f.name, enabled: true, options: { checked: false } }
})
})
.catch(this.toastErrorHandler(this.$t('notification:gateway.filter.fetch.error')))

View File

@@ -0,0 +1,111 @@
<template>
<b-container
class="py-3"
>
<c-content-header
:title="$t('title')"
/>
<div
class="d-flex flex-column h-100"
>
<c-settings-editor
:settings="apigwSettings"
class="mb-3"
@submit="onSettingsSubmit"
/>
<c-route-list
class="mb-4 flex-fill"
/>
</div>
</b-container>
</template>
<script>
import editorHelpers from 'corteza-webapp-admin/src/mixins/editorHelpers'
import CSettingsEditor from 'corteza-webapp-admin/src/components/Apigw/CSettingsEditor'
import CRouteList from 'corteza-webapp-admin/src/components/Apigw/CRouteList'
export default {
components: {
CRouteList,
CSettingsEditor,
},
mixins: [
editorHelpers,
],
i18nOptions: {
namespaces: [ 'system.apigw' ],
},
data () {
return {
settings: {
processing: false,
success: false,
items: [],
},
}
},
computed: {
apigwSettings () {
if (this.settings.items.length > 0) {
return this.settings.items.reduce((map, obj) => {
const { name, value } = obj
const split = name.split('.')
if (split[0] === 'apigw') {
map[name] = value
}
return map
}, {})
}
return {}
},
},
created () {
this.fetchSettings()
},
methods: {
onSettingsSubmit (settings) {
this.settings.processing = true
const values = Object.entries(settings).map(([name, value]) => {
return { name, value }
})
this.$SystemAPI.settingsUpdate({ values })
.then(() => {
this.animateSuccess('settings')
this.toastSuccess(this.$t('notification:settings.system.apigw.success'))
this.$Settings.fetch()
})
.catch(this.toastErrorHandler(this.$t('notification:settings.system.apigw.error')))
.finally(() => {
this.settings.processing = false
})
},
fetchSettings () {
this.incLoader()
this.$SystemAPI.settingsList()
.then((settings = []) => {
this.settings.items = settings
})
.catch(this.toastErrorHandler(this.$t('notification:settings.system.fetch.error')))
.finally(() => {
this.decLoader()
})
},
},
}
</script>

View File

@@ -19,27 +19,36 @@
<h3>
{{ $t('general:label.routes') }}
</h3>
<em>{{ description }}</em>
<div
class="d-flex align-items-center justify-content-between"
class="d-flex align-items-center justify-content-between mt-2"
>
<em>{{ description }}</em>
<div>
<span
:class="{ 'loading': loading }"
>
{{ autoRefreshLabel }}
</span>
<b-button
variant="primary"
data-test-id="button-refresh"
variant="primary"
:disabled="loading"
class="ml-2"
@click="loadItems()"
>
{{ $t('general:label.refresh') }}
</b-button>
<span
class="ml-1"
:class="{ 'loading': loading }"
>
{{ autoRefreshLabel }}
</span>
</div>
<c-input-confirm
:disabled="!items.length"
:borderless="false"
variant="danger"
@confirmed="purgeRequests"
>
{{ $t('purge.all') }}
</c-input-confirm>
</div>
</template>
@@ -63,13 +72,13 @@
>
<template #cell(actions)="row">
<b-button
size="sm"
variant="link"
class="p-0"
:to="{ name: 'system.apigw.profiler.route.list', params: { routeID: row.item.routeID } }"
>
<font-awesome-icon
:icon="['fas', 'pen']"
:icon="['fas', 'info-circle']"
class="text-primary"
/>
</b-button>
</template>
@@ -196,7 +205,7 @@ export default {
},
description () {
return this.$Settings.get('apigw.profilerGlobal', false) ? this.$t('description.globalEnabled') : this.$t('description.globalDisabled')
return this.$Settings.get('apigw.profiler.global', false) ? this.$t('description.globalEnabled') : this.$t('description.globalDisabled')
},
hasNextPage () {
@@ -245,6 +254,15 @@ export default {
})
},
purgeRequests () {
this.$SystemAPI.apigwProfilerPurgeAll()
.then(() => {
this.loadItems()
this.toastSuccess(this.$t('notification:gateway.profiler.purge.success'))
})
.catch(this.toastErrorHandler(this.$t('notification:gateway.profiler.purge.error')))
},
resetItems (sorting = this.sorting) {
this.sorting = sorting
this.filter.before = ''

View File

@@ -9,7 +9,7 @@
class="text-nowrap"
>
<b-button
v-if="$Settings.get('apigw.profilerEnabled', false)"
v-if="$Settings.get('apigw.profiler.enabled', false)"
class="ml-2"
variant="info"
:to="{ name: 'system.apigw.profiler' }"

View File

@@ -96,8 +96,9 @@ export default [
combo('system', 'authClient', { pkey: 'authClientID' }),
combo('system', 'apigw', { pkey: 'routeID', plural: 'routes' }),
r('system.apigw', 'apigw', 'System/Apigw/Index'),
r('system.apigw.new', 'apigw/new', 'System/Apigw/Editor'),
r('system.apigw.edit', 'apigw/edit/:routeID', 'System/Apigw/Editor'),
r('system.apigw.profiler', 'apigw/profiler', 'System/Apigw/Profiler/Index'),
r('system.apigw.profiler.route.list', 'apigw/profiler/route/:routeID', 'System/Apigw/Profiler/Route'),
r('system.apigw.profiler.hit.list', 'apigw/profiler/hit/:hitID', 'System/Apigw/Profiler/Hit'),

View File

@@ -4070,6 +4070,48 @@ export default class System {
return `/apigw/profiler/hit/${hitID}`
}
// Purge all profiler hits
async apigwProfilerPurgeAll (extra: AxiosRequestConfig = {}): Promise<KV> {
const cfg: AxiosRequestConfig = {
...extra,
method: 'post',
url: this.apigwProfilerPurgeAllEndpoint(),
}
return this.api().request(cfg).then(result => stdResolve(result))
}
apigwProfilerPurgeAllEndpoint (): string {
return '/apigw/profiler/purge'
}
// Purge route profiler hits
async apigwProfilerPurge (a: KV, extra: AxiosRequestConfig = {}): Promise<KV> {
const {
routeID,
} = (a as KV) || {}
if (!routeID) {
throw Error('field routeID is empty')
}
const cfg: AxiosRequestConfig = {
...extra,
method: 'post',
url: this.apigwProfilerPurgeEndpoint({
routeID,
}),
}
return this.api().request(cfg).then(result => stdResolve(result))
}
apigwProfilerPurgeEndpoint (a: KV): string {
const {
routeID,
} = a || {}
return `/apigw/profiler/purge/${routeID}`
}
// List resources translations
async localeListResource (a: KV, extra: AxiosRequestConfig = {}): Promise<KV> {
const {

View File

@@ -7,7 +7,7 @@
:size="size"
:disabled="disabled"
:class="`${buttonClass} ${borderless ? 'border-0' : ''}`"
@click.prevent="onPrompt"
@click.stop.prevent="onPrompt"
>
<slot>
<font-awesome-icon
@@ -26,7 +26,7 @@
class="mr-1"
:class="[ borderless && 'border-0' ]"
@blur.prevent="onCancel()"
@click.prevent="onConfirmation()"
@click.prevent.stop="onConfirmation()"
>
<slot name="yes">
<font-awesome-icon
@@ -40,7 +40,7 @@
:size="sizeConfirm"
:disabled="cancelDisabled"
:class="[ borderless && 'border-0' ]"
@click.prevent="onCancel()"
@click.prevent.stop="onCancel()"
>
<slot name="no">
<font-awesome-icon

View File

@@ -156,6 +156,12 @@ gateway:
undelete:
success: Route restored
error: Route restore failed
profiler:
fetch:
error: Requests fetch failed
purge:
success: Requests purged
error: Requests purge failed
workflow:
fetch:
error: Workflow fetch failed
@@ -238,6 +244,9 @@ settings:
smtpCheck:
success: SMTP configuration check passed
error: SMTP configuration check failed
apigw:
success: Integration gateway settings saved
error: Integration gateway settings update failed
compose:
fetch:
error: Compose settings fetch failed

View File

@@ -1,172 +1,177 @@
title: Integration Gateway
list:
export: Export
title: 'Integration Gateway'
new: 'New'
permissions: 'Permissions'
profiler: 'Profiler'
yaml: 'YAML'
loading: 'Loading routes'
title: Routes
new: New
permissions: Permissions
profiler: Profiler
yaml: YAML
loading: Loading routes
numFound: '{{count}} route found'
numFound_plural: '{{count}} routes found'
filterForm:
query:
label: 'Filter route list'
placeholder: 'Filter routes by name'
label: Filter route list
placeholder: Filter routes by name
excluded:
label: 'Without'
label: Without
inclusive:
label: 'Including'
label: Including
exclusive:
label: 'Only'
label: Only
deleted:
label: 'Deleted routes'
label: deleted routes
handle:
placeholder: 'Filter endpoints by path'
placeholder: Filter endpoints by path
columns:
endpoint: 'Endpoint'
createdAt: 'Created'
enabled: 'Enabled'
method: 'Method'
endpoint: Endpoint
createdAt: Created
enabled: Enabled
method: Method
actions: ''
state: State
rows:
filters:
deleted: Deleted
settings:
title: Settings
profiler:
label: Profiler
enabled: Enabled
global: Enabled globally
proxy:
label: Proxy
follow: Follow redirects
editor:
title: 'Edit route'
new: 'New'
permissions: 'Permissions'
title: Edit route
new: New
permissions: Permissions
info:
title: 'Basic information'
title: Basic information
id: 'ID'
endpoint: 'Endpoint'
method: 'Method'
enabled: 'Enabled'
id: ID
endpoint: Endpoint
method: Method
description: Description
enabled: Enabled
delete: 'Delete'
undelete: 'Undelete'
deletedAt: 'Deleted at'
delete: Delete
undelete: Undelete
deletedAt: Deleted at
updatedAt: 'Updated at'
createdAt: 'Created at'
updatedAt: Updated at
createdAt: Created at
tooltip: 'Endpoint must begin with a slash "/" and can not contain any special characters except for the underscore "_" and hyphen "-"'
tooltip: Endpoint must begin with a slash "/" and can not contain any special characters except for the underscore "_" and hyphen "-"
validation:
slash: 'Endpoint must begin with a slash "/"'
minLength: 'Endpoint must contain at least one character'
invalidCharacters: 'Endpoint contains invalid characters'
slash: Endpoint must begin with a slash "/"
minLength: Endpoint must contain at least one character
invalidCharacters: Endpoint contains invalid characters
filters:
title: 'Filter list'
enabled: 'Enabled'
disabled: 'Disabled'
title: Filter list
enabled: Enabled
disabled: Disabled
add: Add
addFilter: Add filter
addHeader: Add header
params: Params
filterListEmpty: Filter list is empty!
modal:
title: 'Query parameters verifier'
ok: 'Save & Close'
title: Query parameters verifier
ok: Save & Close
step_title:
prefilter: 'Prefiltering'
processer: 'Processing'
postfilter: 'Postfiltering'
prefilter: Prefiltering
processer: Processing
postfilter: Postfiltering
list:
remove: 'Remove'
filters: 'Filters'
status: 'Status'
actions: 'Actions'
active: 'Active'
noFilters: 'No filters'
remove: Remove
filters: Filters
status: Status
actions: Actions
active: Active
noFilters: No filters
labels:
expr: 'Expression'
location: 'URL'
workflow: 'Workflow'
status: 'HTTP Status'
jsfunc: 'Function'
input: 'Input'
expr: Expression
location: URL
workflow: Workflow
status: HTTP Status
jsfunc: Function
input: Input
header: Headers
name: Name
value: Value
httpStatus:
none: 'No Status'
'300': '300 - Multiple Choices'
'301': '301 - Moved Permanently'
'302': '302 - Found'
'303': '303 - See Other'
'304': '304 - Not Modified'
'307': '307 - Temporary Redirect'
'308': '308 - Permanent Redirect'
none: No Status
'300': 300 - Multiple Choices
'301': 301 - Moved Permanently
'302': 302 - Found
'303': 303 - See Other
'304': 304 - Not Modified
'307': 307 - Temporary Redirect
'308': 308 - Permanent Redirect
placeholders:
workflow: 'Select a workflow'
add: 'Add'
addFilter: 'Add filter'
params: 'Params'
filterListEmpty: 'Filter list is empty!'
workflow: Select a workflow
help:
expression:
example: 'Example == "match string" && match(SecondParam, "^foo\\s.*$")'
link: 'Documentation'
example: Example == "match string" && match(SecondParam, "^foo\\s.*$")
link: Documentation
profiler:
label: 'Profiler'
title: 'Integration Gateway Profiler'
label: Profiler
title: Integration Gateway Profiler
refreshingIn: Refreshing in {{seconds}}s
description:
globalEnabled: 'Showing routes for all incoming requests'
globalDisabled: 'Showing only registered routes with profiler prefilter'
description:
globalEnabled: Showing routes for all incoming requests
globalDisabled: Showing only registered routes with profiler prefilter
purge:
all: Purge requests for all routes
this: Purge requests for this route
hit:
title: 'Request Details'
title: Request Details
general:
label: General
id: 'Request ID'
route: 'Request Route'
URL: 'Request URL'
method: 'Request Method'
statusCode: 'Status Code'
headers: 'Request Headers'
remoteAddress: 'Remote Address'
duration: 'Duration'
start: 'Start'
end: 'End'
id: Request ID
route: Request Route
URL: Request URL
method: Request Method
statusCode: Status Code
headers: Request Headers
remoteAddress: Remote Address
duration: Duration
start: Start
end: End
openRoute: Open Route
headers:
label: 'Headers'
label: Headers
body:
label: 'Body'
filterForm:
query:
label: 'Filter route list'
placeholder: 'Filter routes by name'
excluded:
label: 'Without'
inclusive:
label: 'Including'
exclusive:
label: 'Only'
deleted:
label: 'Deleted routes'
label: Body
columns:
actions: ''
path: 'Route'
count: 'Count'
content_length: 'Content Length'
http_method: 'Method'
http_status_code: 'Status Code'
size_min: 'Size (min)'
size_max: 'Size (max)'
size_avg: 'Size (avg)'
time_min: 'Time (min)'
time_max: 'Time (max)'
time_avg: 'Time (avg)'
time_start: 'Start time'
time_finish: 'Finish time'
time_duration: 'Duration'
path: Route
count: Count
content_length: Content Length
http_method: Method
http_status_code: Status Code
size_min: Size (min)
size_max: Size (max)
size_avg: Size (avg)
time_min: Time (min)
time_max: Time (max)
time_avg: Time (avg)
time_start: Start time
time_finish: Finish time
time_duration: Duration