feat(frontend): filtering

This commit is contained in:
Dimitrie Stefanescu
2021-12-31 11:39:58 +00:00
parent b3de85f193
commit 7308d09cd3
7 changed files with 242 additions and 158 deletions
@@ -2,7 +2,12 @@
<div class="mt-3">
<portal to="filter-actions">
<v-list-item-action class="pa-0 ma-0">
<v-btn v-tooltip="''" small icon @click.stop="colorBy = !colorBy">
<v-btn
v-tooltip="'Set colors automatically based on each property'"
small
icon
@click.stop="colorBy = !colorBy"
>
<v-icon small :class="`${colorBy ? 'primary--text' : ''}`">mdi-palette</v-icon>
</v-btn>
</v-list-item-action>
@@ -22,21 +27,21 @@
</v-col>
<v-col
v-tooltip="type.fullName"
cols="8"
cols="7"
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 24px"
>
{{ type.name }}
</v-col>
<v-col
cols="3"
cols="4"
:class="`caption text-truncate text-right px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 24px"
>
<div
v-if="colorBy"
class="d-inline-block rounded"
:style="`width: 10px; height: 10px`"
class="d-inline-block rounded mr-3 mt-1 elevation-3"
:style="`width: 8px; height: 8px; background:${legend[type.fullName]};`"
></div>
<v-btn
v-tooltip="'Toggle visibility'"
@@ -74,8 +79,7 @@ export default {
filter: {
type: Object,
default: () => null
},
active: { type: Boolean, default: false }
}
},
data() {
return {
@@ -84,7 +88,7 @@ export default {
typeMap: [],
colorBy: false,
appliedFilter: {},
legend: null
legend: {}
}
},
watch: {
@@ -96,7 +100,6 @@ export default {
? { type: 'category', property: this.filter.targetKey }
: null
let res = await window.__viewer.applyFilter(this.appliedFilter)
console.log(res)
this.mashColorLegend(res.colorLegend)
}
},
@@ -107,9 +110,18 @@ export default {
window.__viewer.applyFilter(null)
},
methods: {
mashColorLegend(thanksCristi) {
if (!thanksCristi) return
// TODO
mashColorLegend(colorLegend) {
// just adds to our colors
if (!colorLegend) return
let keys = Object.keys(colorLegend)
for (const key of keys) {
if (!this.legend[key]) this.$set(this.legend, key, colorLegend[key])
//this.legend[key] = colorLegend[key]
// const idx = this.legend.indexOf((o) => o.name === key)
// if (idx === -1) {
// this.legend.push({ name: key, color: colorLegend[key] })
// }
}
},
async toggleFilter(type) {
let indx = this.filtered.indexOf(type)
@@ -117,9 +129,10 @@ export default {
else this.filtered.splice(indx, 1)
this.hidden.splice(0, this.hidden.length)
if (this.filtered.length === 0) {
window.__viewer.applyFilter(
let res = window.__viewer.applyFilter(
this.colorBy ? { colorBy: { type: 'category', property: this.filter.targetKey } } : null
)
this.mashColorLegend(res.colorLegend)
this.appliedFilter = {}
} else {
let filterObj = {
@@ -139,9 +152,10 @@ export default {
else this.hidden.splice(indx, 1)
this.filtered.splice(0, this.filtered.length)
if (this.hidden.length === 0) {
window.__viewer.applyFilter(
let res = await window.__viewer.applyFilter(
this.colorBy ? { colorBy: { type: 'category', property: this.filter.targetKey } } : null
)
this.mashColorLegend(res.colorLegend)
this.appliedFilter = {}
} else {
let filterObj = {
@@ -0,0 +1,143 @@
<template>
<div class="mt-3">
<portal to="filter-actions">
<v-list-item-action class="pa-0 ma-0">
<v-btn
v-tooltip="'Set colors automatically based on each property'"
small
icon
@click.stop="colorBy = !colorBy"
>
<v-icon small :class="`${colorBy ? 'primary--text' : ''}`">mdi-palette</v-icon>
</v-btn>
</v-list-item-action>
</portal>
<v-row no-gutters class="my-1 property-row rounded-lg">
<v-col
cols="1"
:class="`text-center text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 24px; font-size: 9px"
>
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`">
mdi-information-outline
</v-icon>
</v-col>
<v-col
cols="11"
:class="`caption text-truncatexxx px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 24px"
>
{{ filter.data.objectCount }} elements; min: {{ Math.round(filter.data.minValue, 2) }}; max:
{{ Math.round(filter.data.maxValue, 2) }}
<v-btn
v-show="range[0] !== filter.data.minValue || range[1] !== filter.data.maxValue"
v-tooltip="'Reset'"
x-small
icon
class="mr-1 float-right"
@click="
$set(range, 0, filter.data.minValue)
$set(range, 1, filter.data.maxValue)
setFilter()
"
>
<v-icon class="grey--text mt-1" style="font-size: 12px">mdi-refresh</v-icon>
</v-btn>
</v-col>
<v-col
v-if="filter.data.maxValue === filter.data.minValue"
cols="12"
:class="`caption text-truncatexxx px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 24px"
>
Invalid values (min value equals to max value).
</v-col>
<v-col v-else cols="12" class="mt-5 py-5 px-5">
<v-range-slider
v-model="range"
dense
hide-details
thumb-label="always"
color="primary"
:step="0.01"
:max="filter.data.maxValue"
:min="filter.data.minValue"
:class="`${colorBy ? 'super-slider' : ''}`"
@change="setFilter()"
>
<template #thumb-label="{ value }">{{ value | prettynum }}</template>
</v-range-slider>
</v-col>
</v-row>
</div>
</template>
<script>
export default {
components: {},
props: {
filter: {
type: Object,
default: () => null
},
active: { type: Boolean, default: false }
},
data() {
return {
range: [0, 1],
appliedFilter: {},
legend: {},
colorBy: true
}
},
watch: {
filter(newVal) {
this.$set(this.range, 0, newVal.data.minValue)
this.$set(this.range, 1, newVal.data.maxValue)
},
colorBy() {
this.setFilter()
}
},
mounted() {
this.$set(this.range, 0, this.filter.data.minValue)
this.$set(this.range, 1, this.filter.data.maxValue)
this.setFilter()
},
beforeDestroy() {
window.__viewer.applyFilter(null)
},
methods: {
async setFilter() {
console.log(this.range)
let filterObj = {
filterBy: {},
colorBy: this.colorBy
? {
type: 'gradient',
property: this.filter.targetKey,
minValue: this.range[0],
maxValue: this.range[1],
gradientColors: ['#3F5EFB', '#FC466B']
}
: null,
ghostOthers: true
}
filterObj.filterBy[this.filter.targetKey] = { gte: this.range[0], lte: this.range[1] }
await window.__viewer.applyFilter(filterObj)
}
}
}
</script>
<style>
.property-row {
transition: all 0.3s ease;
background: rgba(120, 120, 120, 0.05);
}
.property-row:hover {
background: rgba(120, 120, 120, 0.09);
}
.super-slider .v-slider__track-fill {
background: linear-gradient(to left, #fc466b, #3f5efb) !important;
background: -webkit-linear-gradient(to left, #fc466b, #3f5efb); /* Chrome 10-25, Safari 5.1-6 */
}
</style>
@@ -0,0 +1,42 @@
<template>
<v-row no-gutters class="my-1 property-row rounded-lg">
<v-col cols="1" class="text-center" style="line-height: 30px">
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`">
{{ filter.data.type === 'number' ? 'mdi-numeric' : 'mdi-format-text' }}
</v-icon>
</v-col>
<v-col
cols="8"
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 30px"
>
<span v-tooltip="filter.targetKey">{{ filter.name }}</span>
<span v-tooltip="'Matching objects'" style="font-size: 10px">
({{ filter.data.objectCount }})
</span>
</v-col>
<v-col class="text-right" style="line-height: 30px">
<v-btn x-small @click="$emit('active-toggle', filter)">Set</v-btn>
</v-col>
</v-row>
</template>
<script>
export default {
components: {},
props: {
filter: {
type: Object,
default: () => null
}
}
}
</script>
<style scoped>
.property-row {
transition: all 0.3s ease;
background: rgba(120, 120, 120, 0.05);
}
.property-row:hover {
background: rgba(120, 120, 120, 0.09);
}
</style>
@@ -1,119 +0,0 @@
<template>
<v-row no-gutters class="my-1 property-row rounded-lg">
<v-col cols="1" class="text-center" style="line-height: 30px">
<v-icon small style="font-size: 12px" :class="`${$vuetify.theme.dark ? 'grey--text' : ''}`">
{{ filter.data.type === 'number' ? 'mdi-numeric' : 'mdi-format-text' }}
</v-icon>
</v-col>
<v-col
v-tooltip="filter.targetKey"
cols="8"
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 30px"
>
{{ filter.name }}
</v-col>
<v-col class="text-right" style="line-height: 30px">
<v-btn x-small @click="$emit('active-toggle', filter)">{{ active ? 'Remove' : 'Set' }}</v-btn>
</v-col>
<v-scroll-y-transition>
<v-col v-if="active && filter.data.type === 'string'" cols="12">
<!-- <div v-if="filter.data.type === 'string'"> -->
<v-row
v-for="type in typeMap"
:key="type.fullName"
no-gutters
class="my-1 property-row rounded-lg"
>
<v-col
cols="1"
:class="`caption text-center text-truncate px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 24px; font-size: 10px"
>
{{ type.count }}
</v-col>
<v-col
v-tooltip="type.fullName"
cols="8"
:class="`caption text-truncate px-1 ${$vuetify.theme.dark ? 'grey--text' : ''}`"
style="line-height: 24px"
>
{{ type.name }}
</v-col>
<v-col
cols="3"
:class="`caption text-truncate text-right px-1 ${
$vuetify.theme.dark ? 'grey--text' : ''
}`"
style="line-height: 24px"
>
<v-btn
v-tooltip="'Toggle visibility'"
x-small
icon
class="mr-1"
@click="toggleVisibility()"
>
<v-icon class="grey--text" style="font-size: 11px">
{{ visible ? 'mdi-eye' : 'mdi-eye-off' }}
</v-icon>
</v-btn>
<v-btn v-tooltip="'Isolate objects'" x-small icon class="mr-1" @click="toggleFilter()">
<v-icon
:class="`${filtered ? 'primary--text' : 'grey--text'}`"
style="font-size: 11px"
>
{{ !filtered ? 'mdi-filter' : 'mdi-filter' }}
</v-icon>
</v-btn>
</v-col>
</v-row>
<!-- </div> -->
</v-col>
</v-scroll-y-transition>
</v-row>
</template>
<script>
export default {
components: {},
props: {
filter: {
type: Object,
default: () => null
},
active: { type: Boolean, default: false }
},
data() {
return {
visible: true,
filtered: false
}
},
computed: {
typeMap() {
if (this.filter.data.type !== 'string') return []
let typeMap = []
for (let key of Object.keys(this.filter.data.uniqueValues)) {
let shortName = key.split('.').reverse()[0]
typeMap.push({
name: shortName,
fullName: key,
count: this.filter.data.uniqueValues[key]
})
}
return typeMap
}
}
}
</script>
<style scoped>
.property-row {
transition: all 0.3s ease;
background: rgba(120, 120, 120, 0.05);
}
.property-row:hover {
background: rgba(120, 120, 120, 0.09);
}
</style>
@@ -39,8 +39,11 @@
</v-list-item>
<v-scroll-y-transition>
<div v-show="expand">
<div v-if="activeFilter" class="px-0">
<filter-active :filter="activeFilter" />
<div v-if="activeFilter && activeFilter.data.type === 'string'">
<filter-category-active :filter="activeFilter" />
</div>
<div v-if="activeFilter && activeFilter.data.type === 'number'">
<filter-numeric-active :filter="activeFilter" />
</div>
<div v-show="activeFilter === null">
<v-subheader>TODO: reccommended filters</v-subheader>
@@ -56,7 +59,7 @@
style="position: sticky; top: 110px; z-index: 6"
/>
<div v-for="filter in matchingFilters" :key="filter.targetKey">
<filter-single :filter="filter" @active-toggle="(e) => (activeFilter = e)" />
<filter-row-select :filter="filter" @active-toggle="(e) => (activeFilter = e)" />
</div>
</div>
</div>
@@ -67,8 +70,9 @@
<script>
export default {
components: {
FilterSingle: () => import('@/cleanup/components/viewer/FilterSingle'),
FilterActive: () => import('@/cleanup/components/viewer/FilterActive')
FilterRowSelect: () => import('@/cleanup/components/viewer/FilterRowSelect'),
FilterCategoryActive: () => import('@/cleanup/components/viewer/FilterCategoryActive'),
FilterNumericActive: () => import('@/cleanup/components/viewer/FilterNumericActive')
},
props: {
props: {
@@ -175,7 +179,6 @@ export default {
filter.data = this.props[key]
filters.push(filter)
}
console.log(filters)
this.allFilters = filters
}
}
@@ -190,7 +193,4 @@ export default {
background: rgba(235, 235, 235, 1);
z-index: 5;
}
.ps {
height: 50vh;
}
</style>
@@ -232,22 +232,26 @@ export default {
this.calls++
if (this.calls !== 2) return
console.log('load start')
await window.__viewer.loadObject(
`${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.commit.referencedObject}`
)
window.__viewer.zoomExtents(undefined, false)
this.loadedModel = true
console.log('load end')
try {
this.objectProperties = await window.__viewer.getObjectsProperties()
} catch (e) {
this.$eventHub.$emit('notification', {
text: 'Failed to get object properties from viewer.'
})
}
this.views.splice(0, this.views.length)
console.log()
this.views.push(...window.__viewer.sceneManager.views)
// TODO: issue when freshly logged in, this throws an error
// that window.__viewer is null.
this.$nextTick(async () => {
await window.__viewer.loadObject(
`${window.location.origin}/streams/${this.stream.id}/objects/${this.stream.commit.referencedObject}`
)
window.__viewer.zoomExtents(undefined, false)
this.loadedModel = true
console.log('load end')
try {
this.objectProperties = await window.__viewer.getObjectsProperties()
} catch (e) {
this.$eventHub.$emit('notification', {
text: 'Failed to get object properties from viewer.'
})
}
this.views.splice(0, this.views.length)
console.log()
this.views.push(...window.__viewer.sceneManager.views)
})
},
captureProgress(args) {
this.loadProgress = args.progress * 100