snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
108
client/web/discovery/src/components/DiscoveryMap.vue
Normal file
108
client/web/discovery/src/components/DiscoveryMap.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<c-map
|
||||
style="height: calc(100vh - 64px);"
|
||||
:map="{
|
||||
zoom,
|
||||
center,
|
||||
}"
|
||||
:markers="makerValues"
|
||||
@on-marker-click="onMarkerClick"
|
||||
@on-map-click="clearClickedMarker"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
const { CMap } = components
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CMap,
|
||||
},
|
||||
|
||||
props: {
|
||||
markers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
hoverIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
zoom: 3,
|
||||
center: [30, 30],
|
||||
rotation: 0,
|
||||
attribution: '© <a target="_blank" rel="noopener noreferrer" href="http://osm.org/copyright">OpenStreetMap</a>',
|
||||
clickedMarker: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
makerValues () {
|
||||
return this.markers.map((marker, i) => {
|
||||
return {
|
||||
id: i,
|
||||
value: marker.coordinates,
|
||||
opacity: [this.hoverIndex, this.clickedMarker].includes(i) ? 1.0 : 0.6,
|
||||
color: 'var(--primary)',
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
markers: {
|
||||
immediate: true,
|
||||
handler (markers = []) {
|
||||
if (markers.length) {
|
||||
const { coordinates = [30, 30] } = markers[0]
|
||||
this.center = coordinates
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
hoverIndex: {
|
||||
handler (hoverIndex) {
|
||||
if (hoverIndex) {
|
||||
const { coordinates } = this.markers.find(({ id }) => id === hoverIndex) || {}
|
||||
if (coordinates) {
|
||||
this.center = coordinates
|
||||
}
|
||||
}
|
||||
|
||||
this.clickedMarker = undefined
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMarkerClick ({ index }) {
|
||||
if (index === this.clickedMarker) {
|
||||
this.clickedMarker = undefined
|
||||
} else {
|
||||
this.clickedMarker = index
|
||||
}
|
||||
|
||||
this.$emit('marker-clicked', this.clickedMarker)
|
||||
},
|
||||
|
||||
clearClickedMarker () {
|
||||
this.clickedMarker = undefined
|
||||
this.$emit('marker-clicked', this.clickedMarker)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vl-style-text {
|
||||
color: var(--white);
|
||||
}
|
||||
</style>
|
||||
197
client/web/discovery/src/components/Filters.vue
Normal file
197
client/web/discovery/src/components/Filters.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- <div class="my-2">
|
||||
<h6 class="text-primary mb-2">
|
||||
{{ $t('types.title') }}
|
||||
</h6>
|
||||
<b-form-checkbox-group
|
||||
:checked="storeResourceTypes"
|
||||
name="type"
|
||||
:disabled="storeProcessing"
|
||||
stacked
|
||||
class="mt-1"
|
||||
@change="updateResourceTypes"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
class="pointer ml-2"
|
||||
>
|
||||
<b-avatar
|
||||
:icon="option.icon"
|
||||
size="sm"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ option.text }}
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div> -->
|
||||
|
||||
<div
|
||||
v-for="agg in aggregationOptions"
|
||||
:key="agg.resource"
|
||||
class="my-2"
|
||||
>
|
||||
<div
|
||||
v-if="agg.items.length"
|
||||
class="d-flex justify-content-between align-items-center"
|
||||
style="min-height: 25px;"
|
||||
>
|
||||
<h6
|
||||
class="text-primary d-flex mb-0"
|
||||
>
|
||||
{{ agg.name }}
|
||||
<b-badge
|
||||
v-if="groups[agg.name].length"
|
||||
variant="light"
|
||||
pill
|
||||
class="ml-1 align-self-center"
|
||||
>
|
||||
{{ groups[agg.name].length }}
|
||||
</b-badge>
|
||||
</h6>
|
||||
<b-button
|
||||
v-if="groups[agg.name].length"
|
||||
variant="link"
|
||||
class="text-muted p-0 m-0"
|
||||
size="sm"
|
||||
@click="clearGroup(agg.name)"
|
||||
>
|
||||
{{ $t('reset') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<b-form-checkbox-group
|
||||
v-model="groups[agg.name]"
|
||||
stacked
|
||||
class="mt-1 ml-2"
|
||||
:disabled="storeProcessing"
|
||||
@change="updateGroup(agg.name)"
|
||||
>
|
||||
<b-form-checkbox
|
||||
v-for="(resource, i) in agg.items"
|
||||
:key="i"
|
||||
:value="resource.name"
|
||||
class="mb-1"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="d-inline-block text-truncate">
|
||||
{{ getResourceDisplayName(agg.resource, resource) }}
|
||||
</span>
|
||||
|
||||
<!-- <span
|
||||
class="pl-3 ml-auto text-muted"
|
||||
>
|
||||
{{ resource.hits }}
|
||||
</span> -->
|
||||
</div>
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
groups: {
|
||||
Module: [],
|
||||
Namespace: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
storeResourceTypes: 'discovery/resourceTypes',
|
||||
storeAggregations: 'discovery/aggregations',
|
||||
storeModules: 'discovery/modules',
|
||||
storeNamespaces: 'discovery/namespaces',
|
||||
storeProcessing: 'discovery/processing',
|
||||
}),
|
||||
|
||||
options () {
|
||||
return [
|
||||
{ text: this.$t('types.namespace'), value: 'compose:namespace', icon: 'code-square' },
|
||||
{ text: this.$t('types.module'), value: 'compose:module', icon: 'list-ul' },
|
||||
{ text: this.$t('types.record'), value: 'compose:record', icon: 'file-earmark-text' },
|
||||
]
|
||||
},
|
||||
|
||||
aggregationOptions () {
|
||||
let namespaceOptions = this.storeAggregations.find(({ resource }) => resource === 'compose:namespace') || {}
|
||||
let moduleOptions = this.storeAggregations.find(({ resource }) => resource === 'compose:module') || {}
|
||||
|
||||
namespaceOptions = {
|
||||
resource: 'compose:namespace',
|
||||
name: 'Namespace',
|
||||
hits: namespaceOptions.hits || 0,
|
||||
items: namespaceOptions.resource_name || [],
|
||||
}
|
||||
|
||||
// Get all modules that are missing from store aggregations but are in filter
|
||||
const missingModuleOptions = this.groups.Module.filter(name => !(moduleOptions.resource_name || []).some(o => o.name === name))
|
||||
.map(name => ({ name }))
|
||||
|
||||
moduleOptions = {
|
||||
resource: 'compose:module',
|
||||
name: 'Module',
|
||||
hits: moduleOptions.hits || 0,
|
||||
items: [
|
||||
...missingModuleOptions,
|
||||
...(moduleOptions.resource_name || []),
|
||||
],
|
||||
}
|
||||
|
||||
return [namespaceOptions, moduleOptions]
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
storeNamespaces: {
|
||||
immediate: true,
|
||||
handler (namespace) {
|
||||
this.groups.Namespace = namespace
|
||||
},
|
||||
},
|
||||
|
||||
storeModules: {
|
||||
immediate: true,
|
||||
handler (module) {
|
||||
this.groups.Module = module
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
updateResourceTypes: 'discovery/updateResourceTypes',
|
||||
}),
|
||||
|
||||
getResourceDisplayName (type, { name, handle, slug }) {
|
||||
if (type === 'compose:namespace') {
|
||||
return name || slug || 'Unnamed namespace'
|
||||
} else if (type === 'compose:module') {
|
||||
return handle || name || 'Unnamed module'
|
||||
}
|
||||
},
|
||||
|
||||
updateGroup (name) {
|
||||
this.$store.dispatch(`discovery/update${name}s`, this.groups[name])
|
||||
},
|
||||
|
||||
clearGroup (name) {
|
||||
this.groups[name] = []
|
||||
this.updateGroup(name)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
90
client/web/discovery/src/components/Results/Module.vue
Normal file
90
client/web/discovery/src/components/Results/Module.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mb-0"
|
||||
>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
>
|
||||
{{ hit.value.namespace.name || hit.value.namespace.handle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
class="mx-2"
|
||||
>
|
||||
<b-icon
|
||||
icon="arrow-right"
|
||||
font-scale="1"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
{{ hit.value.name || hit.value.handle }}
|
||||
</span>
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-badge
|
||||
v-if="Object.keys(hit.value.labels || { }).includes('federation')"
|
||||
variant="light"
|
||||
class="mr-1 h5 p-2 mb-0"
|
||||
>
|
||||
{{ $t('general:federated') }}
|
||||
</b-badge>
|
||||
<b-avatar
|
||||
size="sm"
|
||||
icon="list-ul"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ $t('types.module') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body
|
||||
class="d-flex flex-wrap"
|
||||
style="gap: 2rem;"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="(value, name, i) in limitData()"
|
||||
:key="i"
|
||||
:label="name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%;"
|
||||
>
|
||||
{{ value }}
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'fields',
|
||||
'namespace',
|
||||
'labels',
|
||||
'module',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
65
client/web/discovery/src/components/Results/Namespace.vue
Normal file
65
client/web/discovery/src/components/Results/Namespace.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mb-0"
|
||||
>
|
||||
{{ hit.value.name || hit.value.slug }}
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-avatar
|
||||
size="sm"
|
||||
icon="code-square"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ $t('types.namespace') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body>
|
||||
<div
|
||||
class="d-flex flex-wrap"
|
||||
style="gap: 2rem;"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="(item, name, i) in limitData()"
|
||||
:key="i"
|
||||
:label="name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%;"
|
||||
>
|
||||
{{ item }}
|
||||
</b-form-group>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'namespace',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
176
client/web/discovery/src/components/Results/Record.vue
Normal file
176
client/web/discovery/src/components/Results/Record.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<h5 class="text-primary text-capitalize text-truncate mb-0">
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
>
|
||||
{{ hit.value.namespace.name || hit.value.namespace.handle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hit.value.namespace.name || hit.value.namespace.handle"
|
||||
class="mx-2"
|
||||
>
|
||||
<b-icon
|
||||
icon="arrow-right"
|
||||
font-scale="1"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
{{ hit.value.module.name || hit.value.module.handle }}
|
||||
</span>
|
||||
</h5>
|
||||
|
||||
<span class="text-nowrap">
|
||||
<b-avatar
|
||||
size="sm"
|
||||
icon="file-earmark-text"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
{{ $t('types.record') }}
|
||||
</span>
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="d-flex flex-column flex-wrap gap-2">
|
||||
<div v-if="Object.keys(hit.value.labels || { }).includes('federation')">
|
||||
<b-badge
|
||||
variant="light"
|
||||
class="h6 mb-0"
|
||||
>
|
||||
{{ $t('general:federated') }}
|
||||
</b-badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="recordValues.length"
|
||||
class="d-flex flex-wrap gap-2 flex-grow-1"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="(item, i) in recordValues"
|
||||
:key="i"
|
||||
:label="item.label || item.name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%; white-space: pre-line;"
|
||||
>
|
||||
{{ item.value }}
|
||||
</b-form-group>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="systemValues.length"
|
||||
class="d-flex flex-wrap gap-2 flex-grow-1"
|
||||
>
|
||||
<b-form-group
|
||||
v-for="item in systemValues"
|
||||
:key="item.name"
|
||||
:label="item.label"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 20rem; max-width: 100%;"
|
||||
>
|
||||
<c-user-label
|
||||
v-if="item.name === 'createdBy'"
|
||||
:user="createdByUser"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ item.value }}
|
||||
</template>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import base from './base'
|
||||
|
||||
const { CUserLabel } = components
|
||||
|
||||
const userCache = new Map()
|
||||
const inflight = new Map()
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
components: {
|
||||
CUserLabel,
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
data () {
|
||||
return {
|
||||
enrichedUser: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
recordID () {
|
||||
return this.hit.value.recordID
|
||||
},
|
||||
|
||||
recordValues () {
|
||||
const { values = [] } = this.hit.value
|
||||
|
||||
return (values || []).map(({ name, label, value = [] }) => {
|
||||
if (value) {
|
||||
value = value.map(v => {
|
||||
return (v !== null ? v : '').toString().includes('{"coordinates":[') ? ((JSON.parse(v || '{}') || {}).coordinates || []).join(', ') : v
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
return { name, label, value }
|
||||
})
|
||||
},
|
||||
|
||||
createdByUser () {
|
||||
return this.enrichedUser || this.createdBy || {}
|
||||
},
|
||||
|
||||
systemValues () {
|
||||
return [
|
||||
{ name: 'recordID', label: this.$t('general:recordID'), value: this.recordID },
|
||||
{ name: 'createdBy', label: this.$t('general:createdBy'), value: this.createdBy },
|
||||
{ name: 'createdAt', label: this.$t('general:createdAt'), value: this.createdAt },
|
||||
{ name: 'updatedAt', label: this.$t('general:updatedAt'), value: this.updatedAt },
|
||||
].filter(v => v.value)
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'createdBy.userID': {
|
||||
immediate: true,
|
||||
handler (userID) {
|
||||
if (!userID) {
|
||||
this.enrichedUser = null
|
||||
return
|
||||
}
|
||||
if (userCache.has(userID)) {
|
||||
this.enrichedUser = userCache.get(userID)
|
||||
return
|
||||
}
|
||||
const pending = inflight.get(userID) || this.$SystemAPI.userRead({ userID })
|
||||
.then(user => {
|
||||
userCache.set(userID, user)
|
||||
return user
|
||||
})
|
||||
.finally(() => {
|
||||
inflight.delete(userID)
|
||||
})
|
||||
inflight.set(userID, pending)
|
||||
pending.then(user => {
|
||||
this.enrichedUser = user
|
||||
}).catch(() => {
|
||||
// keep snapshot fallback
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
60
client/web/discovery/src/components/Results/User.vue
Normal file
60
client/web/discovery/src/components/Results/User.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<b-overlay>
|
||||
<b-card-header class="border-bottom">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<h5
|
||||
class="text-primary text-capitalize text-truncate mr-2 mb-0"
|
||||
>
|
||||
{{ $t('types.user') }}
|
||||
</h5>
|
||||
|
||||
<b-avatar
|
||||
v-b-tooltip.noninteractive.hover="{ title: $t('types.user'), boundary: 'body' }"
|
||||
size="sm"
|
||||
class="align-center bg-light text-dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between small">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body class="pb-0">
|
||||
<b-form-group
|
||||
v-for="(value, name, i) in limitData()"
|
||||
:key="i"
|
||||
:label="name"
|
||||
label-class="text-capitalize text-primary"
|
||||
class="mb-0"
|
||||
style="min-width: 200px; max-width: 100%;"
|
||||
>
|
||||
{{ value }}
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'filters',
|
||||
},
|
||||
|
||||
extends: base,
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return [
|
||||
...this.defaultBlacklistedFields,
|
||||
'meta',
|
||||
'fields',
|
||||
'namespace',
|
||||
'labels',
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
79
client/web/discovery/src/components/Results/base.vue
Normal file
79
client/web/discovery/src/components/Results/base.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showMap: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
defaultBlacklistedFields: ['deleted', 'created', 'updated', 'security', 'catch_all'],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
blacklistedFields () {
|
||||
return this.defaultBlacklistedFields
|
||||
},
|
||||
|
||||
query () {
|
||||
return [this.$route.query.query || '']
|
||||
},
|
||||
|
||||
createdBy () {
|
||||
const { by } = this.hit.value.created || {}
|
||||
return by
|
||||
},
|
||||
|
||||
createdAt () {
|
||||
const { at } = this.hit.value.created || {}
|
||||
return at ? this.$options.filters.locFullDateTime(at) : at
|
||||
},
|
||||
|
||||
updatedAt () {
|
||||
const { at } = this.hit.value.updated || {}
|
||||
return at ? this.$options.filters.locFullDateTime(at) : at
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
limitData () {
|
||||
const out = {}
|
||||
|
||||
if (this.hit.value) {
|
||||
for (const key in this.hit.value) {
|
||||
const value = this.hit.value[key]
|
||||
|
||||
if (!!value && this.blacklistedFields.indexOf(key) < 0) {
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.createdBy) {
|
||||
out.createdBy = this.createdBy
|
||||
}
|
||||
|
||||
if (this.createdAt) {
|
||||
out.createdAt = this.createdAt
|
||||
}
|
||||
|
||||
if (this.updatedAt) {
|
||||
out.updatedAt = this.updatedAt
|
||||
}
|
||||
|
||||
return out
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
74
client/web/discovery/src/components/Results/index.vue
Normal file
74
client/web/discovery/src/components/Results/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<b-card
|
||||
no-body
|
||||
class="result shadow-sm h-100"
|
||||
:class="{ 'shadow': hovered }"
|
||||
@mouseover="onHover"
|
||||
@mouseleave="onLeave"
|
||||
>
|
||||
<a
|
||||
v-if="hit.value.url"
|
||||
:href="hit.value.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="stretched-link"
|
||||
/>
|
||||
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="$props"
|
||||
/>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import base from './base'
|
||||
import * as Results from './loader'
|
||||
|
||||
export default {
|
||||
extends: base,
|
||||
|
||||
props: {
|
||||
hit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
hovered: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
component () {
|
||||
const { type } = this.hit
|
||||
const resourceType = type.split(':')[1]
|
||||
|
||||
const keys = Object.keys(Results)
|
||||
const i = keys.map(c => c.toLocaleLowerCase()).findIndex(c => c === resourceType)
|
||||
|
||||
return Results[keys[i]]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onHover () {
|
||||
this.hovered = true
|
||||
this.$emit('hover', this.hit.value.recordID)
|
||||
},
|
||||
|
||||
onLeave () {
|
||||
this.hovered = false
|
||||
this.$emit('hover', undefined)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.result {
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
</style>
|
||||
4
client/web/discovery/src/components/Results/loader.js
Normal file
4
client/web/discovery/src/components/Results/loader.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Module } from './Module'
|
||||
export { default as Namespace } from './Namespace'
|
||||
export { default as Record } from './Record'
|
||||
export { default as User } from './User'
|
||||
445
client/web/discovery/src/components/Search.vue
Normal file
445
client/web/discovery/src/components/Search.vue
Normal file
@@ -0,0 +1,445 @@
|
||||
<template>
|
||||
<b-container
|
||||
fluid
|
||||
class="h-100 mh-100 p-0 d-flex flex-column"
|
||||
>
|
||||
<split
|
||||
direction="horizontal"
|
||||
:gutter-size="12"
|
||||
class="h-100 overflow-hidden"
|
||||
>
|
||||
<split-area
|
||||
:size="map.show ? 70 : 100"
|
||||
:min-size="300"
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<div class="px-3 flex-shrink-0">
|
||||
<b-form-group class="mb-0">
|
||||
<c-input-search
|
||||
:value="query"
|
||||
:placeholder="$t('input-placeholder')"
|
||||
:autocomplete="'off'"
|
||||
:disabled="storeProcessing"
|
||||
submittable
|
||||
@search="onQuerySubmit"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<div
|
||||
class="d-flex align-items-center px-1 mt-1 mb-2 text-muted"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<span
|
||||
:class="{ 'discovering': storeProcessing }"
|
||||
class="mt-1"
|
||||
>
|
||||
{{ searchDescription }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center ml-auto gap-2">
|
||||
<b-button
|
||||
variant="extra-light"
|
||||
size="sm"
|
||||
class="d-flex align-items-center gap-1 mt-2"
|
||||
@click="toggleMap"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'map-marked-alt']"
|
||||
/>
|
||||
{{ !map.show ? $t('search:show-map') : $t('search:hide-map') }}
|
||||
</b-button>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'grip-lines']"
|
||||
class="mt-2 mr-1 pointer"
|
||||
:class="{ 'text-primary': viewMode === 'list' }"
|
||||
@click="viewMode = 'list'"
|
||||
/>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="viewMode"
|
||||
:value="'grid'"
|
||||
:unchecked-value="'list'"
|
||||
switch
|
||||
class="pointer ml-2"
|
||||
/>
|
||||
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'grip-horizontal']"
|
||||
class="mt-2 ml-1 pointer"
|
||||
:class="{ 'text-primary': viewMode === 'grid' }"
|
||||
@click="viewMode = 'grid'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-fill overflow-hidden">
|
||||
<div
|
||||
v-if="(storeProcessing || !total.actual) && !loadingMore"
|
||||
class="d-flex align-items-center justify-content-center w-100 my-5"
|
||||
>
|
||||
<h5 class="mb-0">
|
||||
<b-spinner
|
||||
v-if="storeProcessing"
|
||||
variant="primary"
|
||||
class="p-4"
|
||||
/>
|
||||
<span
|
||||
v-else-if="!total.actual"
|
||||
>
|
||||
{{ $t('no-results') }}
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="results d-flex flex-wrap gap-3 px-4 py-3 overflow-auto"
|
||||
:class="{ 'list-view': viewMode === 'list' }"
|
||||
>
|
||||
<div
|
||||
v-for="(hit, i) in hits"
|
||||
:key="i"
|
||||
class="result-item w-100"
|
||||
:class="{ 'grid-view': viewMode === 'grid' }"
|
||||
>
|
||||
<result
|
||||
:id="`result-${i}`"
|
||||
:index="i"
|
||||
:hit="hit"
|
||||
:show-map="map.show"
|
||||
:class="{ 'border-primary border shadow': map.clickedMarker && map.clickedMarker === i }"
|
||||
class="border"
|
||||
@hover="onResultHover(i)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="total.actual > 0 && total.actual < total.all"
|
||||
class="d-flex align-items-center justify-content-center py-3 w-100"
|
||||
>
|
||||
<b-button
|
||||
variant="primary"
|
||||
:disabled="loadingMore"
|
||||
class="d-flex align-items-center justify-content-center gap-1"
|
||||
@click="getSearchData({ append: true })"
|
||||
>
|
||||
<b-spinner
|
||||
v-if="loadingMore"
|
||||
small
|
||||
/>
|
||||
{{ loadingMore ? $t('search:loading-more') : $t('search:show-more') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</split-area>
|
||||
|
||||
<split-area
|
||||
:size="map.show ? 30 : 0"
|
||||
:min-size="300"
|
||||
>
|
||||
<discovery-map
|
||||
v-if="map.show"
|
||||
:markers="map.markers"
|
||||
:hover-index="map.hoverIndex"
|
||||
class="pl-2"
|
||||
@marker-clicked="markerClicked"
|
||||
/>
|
||||
</split-area>
|
||||
</split>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import Result from './Results'
|
||||
import DiscoveryMap from './DiscoveryMap.vue'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
import { Split, SplitArea } from 'vue-split-panel'
|
||||
const { CInputSearch } = components
|
||||
|
||||
export default {
|
||||
i18nOptions: {
|
||||
namespaces: 'search',
|
||||
},
|
||||
|
||||
components: {
|
||||
Result,
|
||||
DiscoveryMap,
|
||||
CInputSearch,
|
||||
Split,
|
||||
SplitArea,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loadingMore: false,
|
||||
|
||||
query: '',
|
||||
|
||||
hits: [],
|
||||
|
||||
pagination: {
|
||||
limit: 50,
|
||||
from: 0,
|
||||
size: 50,
|
||||
},
|
||||
|
||||
total: {
|
||||
all: 0,
|
||||
actual: 0,
|
||||
},
|
||||
|
||||
initial: false,
|
||||
|
||||
map: {
|
||||
show: false,
|
||||
markers: [],
|
||||
clickedMarker: undefined,
|
||||
hoverIndex: undefined,
|
||||
},
|
||||
|
||||
viewMode: 'list',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
storeProcessing: 'discovery/processing',
|
||||
storeResourceTypes: 'discovery/resourceTypes',
|
||||
storeModules: 'discovery/modules',
|
||||
storeNamespaces: 'discovery/namespaces',
|
||||
}),
|
||||
|
||||
searchDescription () {
|
||||
if (this.storeProcessing) {
|
||||
return this.$t('discovering')
|
||||
}
|
||||
|
||||
if (this.total.all > 0) {
|
||||
return this.$t('range', { actual: this.total.actual, all: this.total.all })
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
storeResourceTypes: {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData()
|
||||
},
|
||||
},
|
||||
|
||||
storeModules: {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData()
|
||||
},
|
||||
},
|
||||
|
||||
storeNamespaces: {
|
||||
handler () {
|
||||
if (this.initial) return
|
||||
this.pagination.size = this.pagination.limit
|
||||
this.getSearchData()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.initial = true
|
||||
|
||||
const { query = '', modules, namespaces, resourceTypes, size = 50 } = this.$route.query
|
||||
|
||||
this.query = query
|
||||
this.pagination.size = size
|
||||
|
||||
if (namespaces) {
|
||||
this.updateNamespaces(Array.isArray(namespaces) ? namespaces : [namespaces])
|
||||
}
|
||||
|
||||
if (modules) {
|
||||
this.updateModules(Array.isArray(modules) ? modules : [modules])
|
||||
}
|
||||
|
||||
if (resourceTypes) {
|
||||
this.updateResourceTypes(Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes])
|
||||
}
|
||||
|
||||
this.getSearchData()
|
||||
|
||||
setTimeout(() => {
|
||||
this.initial = false
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions({
|
||||
fetchData: 'discovery/fetchData',
|
||||
updateModules: 'discovery/updateModules',
|
||||
updateNamespaces: 'discovery/updateNamespaces',
|
||||
updateResourceTypes: 'discovery/updateResourceTypes',
|
||||
}),
|
||||
|
||||
getSearchData ({ query = this.query, append = false } = {}) {
|
||||
if (append) {
|
||||
this.pagination.size += this.pagination.limit
|
||||
this.loadingMore = true
|
||||
} else {
|
||||
this.map.markers = []
|
||||
this.hits = []
|
||||
this.map.hoverIndex = undefined
|
||||
}
|
||||
|
||||
const modules = this.storeModules
|
||||
const namespaces = this.storeNamespaces
|
||||
|
||||
const { size } = this.pagination
|
||||
|
||||
this.updateRouteQuery({ query, modules, namespaces, size })
|
||||
|
||||
this.fetchData({ query, modules, namespaces, size }).then((response = {}) => {
|
||||
if (response) {
|
||||
if (append) {
|
||||
this.hits = [...this.hits, ...(response.hits || [])]
|
||||
} else {
|
||||
this.hits = response.hits || []
|
||||
}
|
||||
this.total.all = response.total_results || 0
|
||||
this.total.actual = this.hits.length
|
||||
|
||||
this.pagination = {
|
||||
...this.pagination,
|
||||
from: response.from || 0,
|
||||
size: response.size || 0,
|
||||
}
|
||||
|
||||
this.getMarkers()
|
||||
}
|
||||
}).catch(e => {
|
||||
this.toastErrorHandler(this.$t('notification:search.failed'))(e)
|
||||
this.hits = []
|
||||
}).finally(() => {
|
||||
this.loadingMore = false
|
||||
})
|
||||
},
|
||||
|
||||
onQuerySubmit (query) {
|
||||
if (!this.storeProcessing) {
|
||||
this.query = query
|
||||
this.pagination.size = 50
|
||||
this.getSearchData()
|
||||
}
|
||||
},
|
||||
|
||||
getMarkers () {
|
||||
const markers = []
|
||||
|
||||
this.hits.forEach(({ type, value }) => {
|
||||
if (type === 'compose:record' && Array.isArray(value.values)) {
|
||||
const id = value.recordID
|
||||
value.values.forEach(({ value = [] }) => {
|
||||
const isGeometry = value && value.find(v => {
|
||||
return (v !== null ? v : '').toString().includes('{"coordinates":[')
|
||||
})
|
||||
|
||||
if (isGeometry) {
|
||||
value.forEach(coordinates => {
|
||||
coordinates = JSON.parse(coordinates || '{}').coordinates
|
||||
if (coordinates && coordinates.length) {
|
||||
markers.push({ id, coordinates })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.map.markers = markers
|
||||
},
|
||||
|
||||
markerClicked (ID) {
|
||||
if (ID) {
|
||||
const result = document.getElementById(`result-${ID}`)
|
||||
|
||||
if (result) {
|
||||
result.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.map.clickedMarker = ID
|
||||
this.map.hoverIndex = undefined
|
||||
},
|
||||
|
||||
onResultHover (index) {
|
||||
if (this.map.show) {
|
||||
this.map.hoverIndex = index
|
||||
}
|
||||
},
|
||||
|
||||
toggleMap () {
|
||||
this.map.show = !this.map.show
|
||||
},
|
||||
|
||||
updateRouteQuery ({ query = undefined, modules = [], namespaces = [], size = 0 }) {
|
||||
if (JSON.stringify(this.$route.query) !== JSON.stringify({ query, modules, namespaces, size })) {
|
||||
this.$router.push({ query: { query: query || undefined, modules, namespaces, size } })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.split .gutter {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// https://stackoverflow.com/a/40991531/17926309
|
||||
.discovering::after {
|
||||
display: inline-block;
|
||||
animation: discovering steps(1, end) 1s infinite;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@keyframes discovering {
|
||||
0% { content: ''; }
|
||||
25% { content: '.'; }
|
||||
50% { content: '..'; }
|
||||
75% { content: '...'; }
|
||||
100% { content: ''; }
|
||||
}
|
||||
|
||||
.result-item {
|
||||
&.grid-view {
|
||||
min-width: 30rem;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-view {
|
||||
.result-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
56
client/web/discovery/src/components/faIcons.js
Normal file
56
client/web/discovery/src/components/faIcons.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGripHorizontal,
|
||||
faGripLines,
|
||||
faThumbtack,
|
||||
faBars,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faSearch,
|
||||
faMapMarkedAlt,
|
||||
faTimes,
|
||||
faLocationArrow,
|
||||
faCheckDouble,
|
||||
faBellSlash,
|
||||
faBell as faBellSolid,
|
||||
faEllipsisV,
|
||||
faCheck,
|
||||
faLocationCrosshairs,
|
||||
faPaperPlane,
|
||||
faRobot,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faQuestionCircle,
|
||||
faUser,
|
||||
faBell,
|
||||
faTrashAlt,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faUser,
|
||||
faQuestionCircle,
|
||||
faGripHorizontal,
|
||||
faThumbtack,
|
||||
faBars,
|
||||
faChevronDown,
|
||||
faChevronUp,
|
||||
faSearch,
|
||||
faMapMarkedAlt,
|
||||
faTimes,
|
||||
faLocationArrow,
|
||||
faCheckDouble,
|
||||
faBell,
|
||||
faBellSolid,
|
||||
faBellSlash,
|
||||
faEllipsisV,
|
||||
faTrashAlt,
|
||||
faGripLines,
|
||||
faCheck,
|
||||
faLocationCrosshairs,
|
||||
faEnvelope,
|
||||
faEnvelopeOpen,
|
||||
faPaperPlane,
|
||||
faRobot,
|
||||
)
|
||||
15
client/web/discovery/src/components/index.js
Normal file
15
client/web/discovery/src/components/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import PortalVue from 'portal-vue'
|
||||
import './faIcons'
|
||||
import { components } from '@cortezaproject/corteza-vue'
|
||||
|
||||
Vue.use(PortalVue)
|
||||
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
||||
Vue.component('font-awesome-layers', FontAwesomeLayers)
|
||||
Vue.component('c-permissions-button', components.CPermissionsButton)
|
||||
Vue.component('c-input-confirm', components.CInputConfirm)
|
||||
Vue.component('c-input-processing', components.CInputProcessing)
|
||||
Vue.component('c-input-search', components.CInputSearch)
|
||||
Reference in New Issue
Block a user