1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-10 15:47:32 +00:00

Dashboard data sources frontend

This commit is contained in:
Petr Stribny 2024-12-25 08:40:24 +00:00
parent 5d683b1168
commit 45e02f5875
52 changed files with 2107 additions and 259 deletions

View file

@ -24,6 +24,7 @@
@import 'select';
@import 'choose_select';
@import 'dropdown';
@import 'dropdown_section';
@import 'field_context';
@import 'tooltip';
@import 'rating';

View file

@ -1,2 +1,11 @@
@import 'components/dashboard_app';
@import 'components/dashboard_app_header';
@import 'dashboard_app';
@import 'dashboard_app_header';
@import 'empty_dashboard';
@import 'dashboard_sidebar';
@import 'empty_dashboard_sidebar';
@import 'create_widget_card';
@import 'dashboard_widget';
@import 'dashboard_summary_widget';
@import 'create_widget_button';
@import 'widget_header';
@import 'widget_settings_base_form';

View file

@ -0,0 +1,11 @@
.create-widget-button {
border: 1px dashed #cdcecf;
box-shadow: 0 1px 2px 0 rgba(7, 8, 16, 0.1);
margin: 24px 0;
padding: 24px;
display: flex;
align-items: center;
justify-content: center;
@include rounded($rounded-md);
}

View file

@ -0,0 +1,37 @@
.create-widget-card {
max-width: 116px;
display: flex;
flex-direction: column;
gap: 14px;
align-items: center;
justify-content: center;
&:hover {
cursor: pointer;
text-decoration: none;
}
}
.create-widget-card__name {
color: $palette-neutral-1200;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
.create-widget-card__img-container {
padding: 16px 24px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
border: 1px solid #ededed;
background: #fafafa;
box-shadow: 0 1px 2px 0 rgba(7, 8, 16, 0.1);
}
.create-widget-card:hover .create-widget-card__img-container {
border: 1px solid #d7d8d9;
background: #f7f7f7;
}

View file

@ -1,14 +1,32 @@
.dashboard-app__layout {
display: flex;
}
.dashboard-app__layout-scrollable {
height: 100%;
overflow: auto;
}
.dashboard-app__content {
min-height: calc(100vh - 100px);
min-width: 400px;
display: flex;
flex-direction: column;
margin: 24px 20px 24px 24px;
padding: 56px 80px;
background-color: $white;
border: 1px solid $palette-neutral-200;
box-shadow: 0 1px 2px 0 rgba(7, 8, 16, 0.1);
@include rounded($rounded);
@media screen and (max-width: $dashboard-breakpoint) {
padding: 20px 24px;
}
}
.dashboard-app__content-header {
margin: 56px 80px 0;
margin-bottom: 24px;
}
.dashboard-app__title {

View file

@ -0,0 +1,5 @@
.dashboard-sidebar {
background: #fff;
padding: 16px 16px 14px;
border-left: 1px solid $palette-neutral-200;
}

View file

@ -0,0 +1,18 @@
.dashboard-summary-widget {
padding: 0 24px 24px;
}
.dashboard-summary-widget__summary {
color: $palette-neutral-1200;
font-size: 40px;
font-weight: 600;
line-height: 40px;
margin-top: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dashboard-summary-widget__summary--misconfigured {
color: #cdcecf;
}

View file

@ -0,0 +1,41 @@
.dashboard-widget {
border: 1px solid $palette-neutral-200;
box-shadow: 0 1px 2px 0 rgba(7, 8, 16, 0.1);
position: relative;
&:not(:first-child) {
margin-top: 24px;
}
@include rounded($rounded-md);
}
.dashboard-widget--selected {
outline: 1.5px solid #4e5cfe;
outline-offset: 8px;
}
.dashboard-widget--selectable {
border: 1px dashed #cdcecf;
&:hover {
cursor: pointer;
}
}
.dashboard-widget__name {
@include elevation($elevation-low);
@include absolute(-37px, auto, auto, 0);
@include rounded(80px);
font-size: 10px;
font-style: normal;
font-weight: 500;
line-height: 12px;
letter-spacing: 0.2px;
background-color: #4e5cfe;
color: $white;
cursor: default;
padding: 4px 8px;
z-index: 1;
}

View file

@ -0,0 +1,34 @@
.empty-dashboard {
display: grid;
place-items: center;
flex-grow: 2;
margin: 32px 0;
border-radius: 6px;
border: 1px dashed #d7d8d9;
padding: 20px 0;
}
.empty-dashboard__content {
width: 50%;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-dashboard__content-title {
color: $palette-neutral-1200;
text-align: center;
font-size: 16px;
font-weight: 500;
line-height: 20px;
margin-bottom: 12px;
}
.empty-dashboard__content-subtitle {
text-align: center;
color: #6a6b70;
font-size: 12px;
font-weight: 400;
line-height: 20px;
margin-bottom: 24px;
}

View file

@ -0,0 +1,37 @@
.empty-dashboard-sidebar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 0 20%;
text-align: center;
}
.empty-dashboard-sidebar__icon {
font-size: 20px;
color: $color-neutral-400;
margin-bottom: 24px;
padding: 12px;
border-radius: 6px;
border: 1px solid $palette-neutral-200;
box-shadow: 0 1px 2px 0 rgba(7, 8, 16, 0.1);
}
.empty-dashboard-sidebar__title {
color: $palette-neutral-1200;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 20px;
margin-bottom: 16px;
}
.empty-dashboard-sidebar__message {
color: $palette-neutral-900;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}

View file

@ -0,0 +1,57 @@
.widget-header {
display: flex;
flex-direction: row;
align-items: start;
gap: 7px;
}
.widget-header__main {
padding-top: 24px;
overflow: hidden;
}
.widget-header__context-menu {
flex-grow: 1;
text-align: right;
margin: 6px -14px 0 7px;
width: 46px;
}
.widget-header__title-wrapper {
display: flex;
gap: 7px;
}
.widget-header__title {
color: $palette-neutral-1200;
font-size: 16px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.widget-header__description {
color: #6a6b70;
font-size: 12px;
font-weight: 400;
margin-top: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.widget-header__fix-configuration {
display: flex;
align-items: center;
gap: 6px;
color: #b23f30;
font-size: 10px;
font-weight: 500;
line-height: 12px;
letter-spacing: 0.2px;
padding: 4px 8px;
border-radius: 80px;
background: #fff2f0;
white-space: nowrap;
}

View file

@ -0,0 +1,5 @@
.widget-settings-base-form {
border-bottom: 1px solid $color-neutral-200;
padding-bottom: 20px;
margin-bottom: 20px;
}

View file

@ -0,0 +1,22 @@
.dropdown-section {
margin-bottom: 4px;
display: none;
&:last-child {
margin-bottom: 0;
}
&:has(.select__item.visible) {
display: block;
}
}
.dropdown-section__title {
padding: 12px 0 8px 12px;
color: $palette-neutral-900;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 11px;
letter-spacing: 0.22px;
}

View file

@ -188,6 +188,10 @@
}
}
.select__item-link--indented {
padding-left: 14px;
}
.select__item-name {
display: flex;
align-items: center;

View file

@ -61,7 +61,7 @@ $file-field-modal-foot-height: 108px !default;
$onboarding-breakpoint: 920px !default;
$dashboard-breakpoint: 1100px;
$dashboard-breakpoint: 900px;
$builder-page-max-width: 1280px;

View file

@ -4,6 +4,7 @@
class="select__item select__item--no-options"
:class="{
hidden: !isVisible(query),
visible: isVisible(query),
active: isActive(value),
disabled: disabled,
hover: isHovering(value),
@ -12,6 +13,7 @@
>
<a
class="select__item-link"
:class="{ 'select__item-link--indented': indented }"
@click="select(value, disabled)"
@mousemove="hover(value, disabled)"
>

View file

@ -0,0 +1,18 @@
<template>
<div class="dropdown-section">
<div class="dropdown-section__title">{{ title }}</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'DropdownSection',
props: {
title: {
type: String,
required: true,
},
},
}
</script>

View file

@ -45,6 +45,11 @@ export default {
required: false,
default: true,
},
indented: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {

View file

@ -23,6 +23,11 @@ export default {
return {
// A list of values that the form allows. If null all values are allowed.
allowedValues: null,
// By setting emitValuesOnReset to false in the form's component
// the values changed event won't be sent right after resetting the
// form
emitValuesOnReset: true,
isAfterReset: true,
}
},
mounted() {
@ -201,6 +206,8 @@ export default {
* first level of children.
*/
async reset(deep = false) {
this.isAfterReset = true
Object.assign(
this.values,
this.$options.data.call(this).values,
@ -237,7 +244,13 @@ export default {
},
emitChange(newValues) {
this.$emit('values-changed', newValues)
if (this.emitValuesOnReset === true || this.isAfterReset === false) {
this.$emit('values-changed', newValues)
}
if (this.isAfterReset) {
this.isAfterReset = false
}
},
},
}

View file

@ -4,6 +4,7 @@ import Context from '@baserow/modules/core/components/Context'
import Modal from '@baserow/modules/core/components/Modal'
import Editable from '@baserow/modules/core/components/Editable'
import Dropdown from '@baserow/modules/core/components/Dropdown'
import DropdownSection from '@baserow/modules/core/components/DropdownSection'
import DropdownItem from '@baserow/modules/core/components/DropdownItem'
import Picker from '@baserow/modules/core/components/Picker'
import ProgressBar from '@baserow/modules/core/components/ProgressBar'
@ -65,6 +66,7 @@ function setupVue(Vue) {
Vue.component('Modal', Modal)
Vue.component('Editable', Editable)
Vue.component('Dropdown', Dropdown)
Vue.component('DropdownSection', DropdownSection)
Vue.component('DropdownItem', DropdownItem)
Vue.component('Checkbox', Checkbox)
Vue.component('Radio', Radio)

View file

@ -0,0 +1,9 @@
<svg width="72" height="40" viewBox="0 0 72 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.25" y="1.25" width="69.5" height="37.5" rx="4.75" fill="white"/>
<rect x="1.25" y="1.25" width="69.5" height="37.5" rx="4.75" stroke="#E6E6E7" stroke-width="1.5"/>
<rect opacity="0.48" x="12" y="28" width="14" height="4" rx="2" fill="#E6E6E7"/>
<rect opacity="0.48" x="47" y="28" width="13" height="4" rx="2" fill="#E6E6E7"/>
<path d="M14.2539 18.6816V16.4551H12.0332V14.9434H14.2539V12.7168H15.7656V14.9434H17.998V16.4551H15.7656V18.6816H14.2539ZM19.1816 18.4824V17.0527L22.7734 11.375H25.0586V17.0234H26.1309V18.4824H25.0586V20H23.3184V18.4824H19.1816ZM23.3477 17.0234V13.3672H23.2832L21.0156 16.9531V17.0234H23.3477Z" fill="#0D9439"/>
<path d="M51.9414 14.9727V16.3906H48.0098V14.9727H51.9414ZM52.832 20V18.6816L55.8965 15.8398C56.6699 15.0898 57.0918 14.6152 57.1035 13.9297C57.0918 13.1738 56.5293 12.6992 55.7793 12.7051C54.9824 12.6992 54.4668 13.2031 54.4727 14.0352H52.7441C52.7324 12.3359 53.9746 11.2578 55.791 11.2578C57.6309 11.2578 58.8555 12.3125 58.8672 13.8125C58.8555 14.791 58.375 15.5996 56.6113 17.2168L55.3516 18.4473V18.5059H58.9844V20H52.832Z" fill="#B23F30"/>
<rect x="36" y="12" width="1" height="16" fill="#EDEDED"/>
</svg>

After

(image error) Size: 1.2 KiB

View file

@ -0,0 +1,34 @@
<template>
<div class="create-widget-button">
<CreateWidgetModal
ref="createWidgetModal"
:dashboard="dashboard"
@widget-type-selected="$emit('widget-type-selected', $event)"
/>
<ButtonFloating
icon="iconoir-plus"
type="secondary"
@click="openCreateWidgetModal"
></ButtonFloating>
</div>
</template>
<script>
import CreateWidgetModal from '@baserow/modules/dashboard/components/CreateWidgetModal'
export default {
name: 'CreateWidgetButton',
components: { CreateWidgetModal },
props: {
dashboard: {
type: Object,
required: true,
},
},
methods: {
openCreateWidgetModal() {
this.$refs.createWidgetModal.show()
},
},
}
</script>

View file

@ -0,0 +1,48 @@
<template>
<Modal>
<h2 class="box__title">
{{ $t('createWidgetModal.title') }}
</h2>
<div>
<a
v-for="widgetType in widgetTypes"
:key="widgetType.type"
class="create-widget-card"
@click="widgetTypeSelected(widgetType.type)"
>
<div class="create-widget-card__img-container">
<img :src="widgetType.createWidgetImage" />
</div>
<div class="create-widget-card__name">
{{ widgetType.name }}
</div>
</a>
</div>
</Modal>
</template>
<script>
import modal from '@baserow/modules/core/mixins/modal'
export default {
name: 'CreateWidgetModal',
mixins: [modal],
props: {
dashboard: {
type: Object,
required: true,
},
},
computed: {
widgetTypes() {
return this.$registry.getAll('dashboardWidget')
},
},
methods: {
widgetTypeSelected(widgetType) {
this.$emit('widget-type-selected', widgetType)
this.hide()
},
},
}
</script>

View file

@ -0,0 +1,104 @@
<template>
<div>
<div v-if="!isLoading">
<div class="layout__col-2-2 dashboard-app__layout">
<div
class="dashboard-app__layout-scrollable"
:style="{ width: `calc(100% - ${sidebarWidth}px)` }"
>
<div class="dashboard-app__content">
<DashboardContentHeader :dashboard="dashboard" />
<EmptyDashboard
v-if="isEmpty"
:dashboard="dashboard"
@widget-type-selected="createWidget($event)"
/>
<template v-else>
<WidgetBoard :dashboard="dashboard" />
<CreateWidgetButton
v-if="isEditMode && canCreateWidget"
:dashboard="dashboard"
@widget-type-selected="createWidget($event)"
/>
</template>
</div>
</div>
<DashboardSidebar
v-if="isEditMode"
:dashboard="dashboard"
:style="{ width: `${sidebarWidth}px` }"
/>
</div>
</div>
</div>
</template>
<script>
import EmptyDashboard from '@baserow/modules/dashboard/components/EmptyDashboard'
import CreateWidgetButton from '@baserow/modules/dashboard/components/CreateWidgetButton'
import DashboardSidebar from '@baserow/modules/dashboard/components/DashboardSidebar'
import DashboardContentHeader from '@baserow/modules/dashboard/components/DashboardContentHeader'
import WidgetBoard from '@baserow/modules/dashboard/components/WidgetBoard'
import { mapGetters, mapActions } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
name: 'DashboardContent',
components: {
EmptyDashboard,
CreateWidgetButton,
WidgetBoard,
DashboardContentHeader,
DashboardSidebar,
},
props: {
dashboard: {
type: Object,
required: true,
},
},
data() {
return {
contentHeight: 0,
}
},
computed: {
...mapGetters({
isEditMode: 'dashboardApplication/isEditMode',
isEmpty: 'dashboardApplication/isEmpty',
isLoading: 'dashboardApplication/isLoading',
}),
sidebarWidth() {
if (this.isEditMode) {
return 352
}
return 0
},
},
methods: {
...mapActions({
toggleEditMode: 'dashboardApplication/toggleEditMode',
enterEditMode: 'dashboardApplication/enterEditMode',
}),
canCreateWidget() {
return this.$hasPermission(
'dashboard.create_widget',
this.dashboard,
this.dashboard.workspace.id
)
},
async createWidget(widgetType) {
const typeFromRegistry = this.$registry.get('dashboardWidget', widgetType)
try {
await this.$store.dispatch('dashboardApplication/createWidget', {
dashboard: this.dashboard,
widget: { title: typeFromRegistry.name, type: widgetType },
})
this.enterEditMode()
} catch (error) {
notifyIf(error, 'dashboard')
}
},
},
}
</script>

View file

@ -0,0 +1,112 @@
<template>
<div class="dashboard-app__content-header">
<div class="dashboard-app__title">
<Editable
ref="dashboardNameEditable"
:value="dashboard.name"
@change="renameApplication(dashboard, $event)"
@editing="editingDashboardName = $event"
></Editable>
<a
v-if="isEditMode"
class="dashboard-app__edit"
:class="{ 'visibility-hidden': editingDashboardName }"
@click="editName"
>
<i class="dashboard-app__edit-icon iconoir-edit-pencil"></i
></a>
</div>
<div
v-if="isEditMode || dashboard.description"
class="dashboard-app__description"
>
<Editable
ref="dashboardDescriptionEditable"
:value="dashboard.description"
:placeholder="$t('dashboard.descriptionPlaceholder')"
@change="updateDescription(dashboard, $event)"
@editing="editingDashboardDescription = $event"
></Editable>
<a
v-if="isEditMode"
class="dashboard-app__edit"
:class="{ 'visibility-hidden': editingDashboardDescription }"
@click="editDescription"
>
<i class="dashboard-app__edit-icon iconoir-edit-pencil"></i
></a>
</div>
</div>
</template>
<script>
import { notifyIf } from '@baserow/modules/core/utils/error'
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'DashboardContentHeader',
props: {
dashboard: {
type: Object,
required: true,
},
},
data() {
return {
editingDashboardName: false,
editingDashboardDescription: false,
}
},
computed: {
...mapGetters({
isEditMode: 'dashboardApplication/isEditMode',
isEmpty: 'dashboardApplication/isEmpty',
}),
},
methods: {
...mapActions({
selectWidget: 'dashboardApplication/selectWidget',
}),
editName() {
this.$refs.dashboardNameEditable.edit()
this.editingDashboardName = true
this.selectWidget(null)
},
editDescription() {
this.$refs.dashboardDescriptionEditable.edit()
this.editingDashboardDescription = true
this.selectWidget(null)
},
async renameApplication(application, event) {
try {
await this.$store.dispatch('application/update', {
application,
values: {
name: event.value,
},
})
} catch (error) {
this.$refs.dashboardNameEditable.set(event.oldValue)
notifyIf(error, 'application')
} finally {
this.editingDashboardName = false
}
},
async updateDescription(application, event) {
try {
await this.$store.dispatch('application/update', {
application,
values: {
description: event.value,
},
})
} catch (error) {
this.$refs.dashboardDescriptionEditable.set(event.oldValue)
notifyIf(error, 'application')
} finally {
this.editingDashboardDescription = false
}
},
},
}
</script>

View file

@ -1,11 +1,14 @@
<template>
<header class="layout__col-2-1 header header--space-between">
<DashboardHeaderMenuItems v-if="!isEditMode" :dashboard="dashboard" />
<div v-else class="dashboard-app-header__done-editing">
<Button type="primary" @click="doneEditing">{{
$t('dashboardHeader.doneEditing')
}}</Button>
</div>
<div v-show="isLoading" class="header__loading"></div>
<template v-if="!isLoading">
<DashboardHeaderMenuItems v-if="!isEditMode" :dashboard="dashboard" />
<div v-else class="dashboard-app-header__done-editing">
<Button type="primary" @click="doneEditing">{{
$t('dashboardHeader.doneEditing')
}}</Button>
</div>
</template>
</header>
</template>
@ -27,6 +30,7 @@ export default {
computed: {
...mapGetters({
isEditMode: 'dashboardApplication/isEditMode',
isLoading: 'dashboardApplication/isLoading',
}),
},
methods: {

View file

@ -0,0 +1,32 @@
<template>
<div class="dashboard-sidebar">
<WidgetSettings
v-if="selectedWidget"
:dashboard="dashboard"
:widget="selectedWidget"
/>
<EmptyDashboardSidebar v-else />
</div>
</template>
<script>
import EmptyDashboardSidebar from '@baserow/modules/dashboard/components/EmptyDashboardSidebar'
import WidgetSettings from '@baserow/modules/dashboard/components/widget/WidgetSettings'
import { mapGetters } from 'vuex'
export default {
name: 'DashboardSidebar',
components: { EmptyDashboardSidebar, WidgetSettings },
props: {
dashboard: {
type: Object,
required: true,
},
},
computed: {
...mapGetters({
selectedWidget: 'dashboardApplication/getSelectedWidget',
}),
},
}
</script>

View file

@ -0,0 +1,51 @@
<template>
<div class="empty-dashboard">
<div class="empty-dashboard__content">
<div class="empty-dashboard__content-title">
{{ $t('emptyDashboard.title') }}
</div>
<div v-if="canCreateWidget" class="empty-dashboard__content-subtitle">
{{ $t('emptyDashboard.subtitle') }}
</div>
<Button
v-if="canCreateWidget"
type="primary"
icon="iconoir-plus"
@click="openCreateWidgetModal"
>{{ $t('emptyDashboard.addWidget') }}</Button
>
</div>
<CreateWidgetModal
ref="createWidgetModal"
:dashboard="dashboard"
@widget-type-selected="$emit('widget-type-selected', $event)"
/>
</div>
</template>
<script>
import CreateWidgetModal from '@baserow/modules/dashboard/components/CreateWidgetModal'
export default {
name: 'EmptyDashboard',
components: { CreateWidgetModal },
props: {
dashboard: {
type: Object,
required: true,
},
},
methods: {
openCreateWidgetModal() {
this.$refs.createWidgetModal.show()
},
canCreateWidget() {
return this.$hasPermission(
'dashboard.create_widget',
this.dashboard,
this.dashboard.workspace.id
)
},
},
}
</script>

View file

@ -0,0 +1,17 @@
<template>
<div class="empty-dashboard-sidebar">
<i class="iconoir-cursor-pointer empty-dashboard-sidebar__icon"></i>
<div class="empty-dashboard-sidebar__title">
{{ $t('emptyDashboardSidebar.title') }}
</div>
<div class="empty-dashboard-sidebar__message">
{{ $t('emptyDashboardSidebar.message') }}
</div>
</div>
</template>
<script>
export default {
name: 'EmptyDashboardSidebar',
}
</script>

View file

@ -0,0 +1,32 @@
<template>
<div>
<DashboardWidget
v-for="widget in widgets"
:key="widget.id"
:widget="widget"
:dashboard="dashboard"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import DashboardWidget from '@baserow/modules/dashboard/components/widget/DashboardWidget'
export default {
name: 'WidgetBoard',
components: { DashboardWidget },
props: {
dashboard: {
type: Object,
required: true,
},
},
computed: {
...mapGetters({
isEditMode: 'dashboardApplication/isEditMode',
widgets: 'dashboardApplication/getWidgets',
}),
},
}
</script>

View file

@ -0,0 +1,303 @@
<template>
<form @submit.prevent>
<FormSection
:title="$t('aggregateRowsDataSourceForm.data')"
class="margin-bottom-2"
>
<FormGroup
:label="$t('aggregateRowsDataSourceForm.sourceFieldLabel')"
class="margin-bottom-2"
small-label
required
horizontal
horizontal-narrow
>
<Dropdown
v-model="values.table_id"
:show-search="true"
fixed-items
:error="fieldHasErrors('table_id')"
@change="$v.values.table_id.$touch()"
>
<DropdownSection
v-for="database in databases"
:key="database.id"
:title="`${database.name} (${database.id})`"
>
<DropdownItem
v-for="table in database.tables"
:key="table.id"
:name="table.name"
:value="table.id"
:indented="true"
>
{{ table.name }}
</DropdownItem>
</DropdownSection>
</Dropdown>
</FormGroup>
<FormGroup
v-if="values.table_id && !fieldHasErrors('table_id')"
:label="$t('aggregateRowsDataSourceForm.viewFieldLabel')"
class="margin-bottom-2"
small-label
required
horizontal
horizontal-narrow
>
<Dropdown
v-model="values.view_id"
:show-search="false"
fixed-items
:error="fieldHasErrors('view_id')"
@change="$v.values.view_id.$touch()"
>
<DropdownItem
:name="$t('aggregateRowsDataSourceForm.notSelected')"
:value="null"
>{{ $t('aggregateRowsDataSourceForm.notSelected') }}</DropdownItem
>
<DropdownItem
v-for="view in tableViews"
:key="view.id"
:name="view.name"
:value="view.id"
>
{{ view.name }}
</DropdownItem>
</Dropdown>
</FormGroup>
<FormGroup
v-if="values.table_id && !fieldHasErrors('table_id')"
class="margin-bottom-2"
small-label
:label="$t('aggregateRowsDataSourceForm.aggregationFieldLabel')"
required
horizontal
horizontal-narrow
>
<Dropdown
v-model="values.field_id"
:disabled="tableFields.length === 0"
:error="fieldHasErrors('field_id')"
@change="$v.values.field_id.$touch()"
>
<DropdownItem
v-for="field in tableFields"
:key="field.id"
:name="field.name"
:value="field.id"
:icon="fieldIconClass(field)"
>
</DropdownItem>
</Dropdown>
</FormGroup>
<FormGroup
v-if="!fieldHasErrors('field_id')"
small-label
:label="$t('aggregateRowsDataSourceForm.aggregationTypeLabel')"
required
horizontal
horizontal-narrow
>
<Dropdown
v-model="values.aggregation_type"
:error="fieldHasErrors('aggregation_type')"
@change="$v.values.aggregation_type.$touch()"
>
<DropdownItem
v-for="viewAggregation in viewAggregationTypes"
:key="viewAggregation.getType()"
:name="viewAggregation.getName()"
:value="viewAggregation.getType()"
>
</DropdownItem>
</Dropdown>
</FormGroup>
</FormSection>
</form>
</template>
<script>
import form from '@baserow/modules/core/mixins/form'
import { required } from 'vuelidate/lib/validators'
const includes = (array) => (value) => {
return array.includes(value)
}
const includesIfSet = (array) => (value) => {
if (value === null || value === undefined) {
return true
}
return array.includes(value)
}
export default {
name: 'AggregateRowsDataSourceForm',
mixins: [form],
props: {
dashboard: {
type: Object,
required: true,
},
widget: {
type: Object,
required: true,
},
dataSource: {
type: Object,
required: true,
},
},
data() {
return {
allowedValues: ['table_id', 'view_id', 'field_id', 'aggregation_type'],
values: {
table_id: null,
view_id: null,
field_id: null,
aggregation_type: 'sum',
},
tableLoading: false,
databaseSelectedId: null,
emitValuesOnReset: false,
}
},
computed: {
integration() {
return this.$store.getters['dashboardApplication/getIntegrationById'](
this.dataSource.integration_id
)
},
databases() {
return this.integration.context_data.databases
},
databaseSelected() {
return this.databases.find(
(database) => database.id === this.databaseSelectedId
)
},
tables() {
return this.databases.map((database) => database.tables).flat()
},
tableIds() {
return this.tables.map((table) => table.id)
},
tableSelected() {
return this.tables.find(({ id }) => id === this.values.table_id)
},
tableFields() {
return this.tableSelected?.fields || []
},
tableFieldIds() {
return this.tableFields.map((field) => field.id)
},
tableViews() {
return (
this.databaseSelected?.views.filter(
(view) => view.table_id === this.values.table_id
) || []
)
},
tableViewIds() {
return this.tableViews.map((view) => view.id)
},
viewAggregationTypes() {
const selectedField = this.tableFields.find(
(field) => field.id === this.values.field_id
)
if (!selectedField) return []
return this.$registry
.getOrderedList('viewAggregation')
.filter((agg) => agg.fieldIsCompatible(selectedField))
},
aggregationTypeNames() {
return this.viewAggregationTypes.map((aggType) => aggType.getType())
},
},
watch: {
dataSource: {
handler() {
// Reset the form to set default values
// again after a different widget is selected
this.reset(true)
// Run form validation so that
// problems are highlighted immediately
this.$v.$touch()
},
immediate: true,
},
'values.table_id': {
handler(tableId) {
if (tableId !== null) {
const databaseOfTableId = this.databases.find((database) =>
database.tables.some((table) => table.id === tableId)
)
if (databaseOfTableId) {
this.databaseSelectedId = databaseOfTableId.id
}
// If the values are not changed by the user
// we don't want to continue with preselecting
// default values
if (tableId === this.defaultValues.table_id) {
return
}
if (
!this.tableViews.some((view) => view.id === this.values.view_id)
) {
this.values.view_id = null
}
if (
!this.tableFields.some((field) => field.id === this.values.field_id)
) {
if (this.tableFields.length > 0) {
this.values.field_id = this.tableFields[0].id
}
}
}
},
immediate: true,
},
'values.field_id': {
handler(fieldId) {
if (fieldId !== null) {
if (
!this.viewAggregationTypes.some(
(agg) => agg.getType() === this.values.aggregation_type
)
) {
if (this.viewAggregationTypes.length > 0) {
this.values.aggregation_type =
this.viewAggregationTypes[0].getType()
}
}
}
},
immediate: false,
},
},
validations() {
return {
values: {
table_id: { required, isValidTableId: includesIfSet(this.tableIds) },
view_id: { isValidViewId: includesIfSet(this.tableViewIds) },
field_id: { required, isValidFieldId: includes(this.tableFieldIds) },
aggregation_type: {
required,
isValidAggregationType: includes(this.aggregationTypeNames),
},
},
}
},
methods: {
fieldIconClass(field) {
const fieldType = this.$registry.get('field', field.type)
return fieldType.iconClass
},
},
}
</script>

View file

@ -0,0 +1,73 @@
<template>
<div
class="dashboard-widget"
:class="{
'dashboard-widget--selected': isSelected,
'dashboard-widget--selectable': isSelectable,
}"
@click="selectWidgetIfAllowed(widget.id)"
>
<div v-if="isSelected && isEditMode" class="dashboard-widget__name">
{{ widgetType.name }}
</div>
<component
:is="widgetComponent(widget.type)"
:dashboard="dashboard"
:widget="widget"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'DashboardWidget',
props: {
dashboard: {
type: Object,
required: true,
},
widget: {
type: Object,
required: true,
},
},
computed: {
...mapGetters({
selectedWidgetId: 'dashboardApplication/getSelectedWidgetId',
isEditMode: 'dashboardApplication/isEditMode',
}),
isSelected() {
return this.selectedWidgetId === this.widget.id && this.isEditMode
},
isSelectable() {
return this.selectedWidgetId !== this.widget.id && this.isEditMode
},
widgetType() {
return this.$registry.get('dashboardWidget', this.widget.type)
},
},
methods: {
...mapActions({
selectWidget: 'dashboardApplication/selectWidget',
}),
widgetComponent(type) {
const widgetType = this.$registry.get('dashboardWidget', type)
return widgetType.component
},
selectWidgetIfAllowed(widgetId) {
if (this.canSelectWidget()) {
this.selectWidget(widgetId)
}
},
canSelectWidget() {
return this.$hasPermission(
'dashboard.widget.update',
this.widget,
this.dashboard.workspace.id
)
},
},
}
</script>

View file

@ -0,0 +1,88 @@
<template>
<div class="dashboard-summary-widget">
<div class="widget-header">
<div class="widget-header__main">
<div class="widget-header__title-wrapper">
<div class="widget-header__title">{{ widget.title }}</div>
<div
v-if="dataSourceMisconfigured"
class="widget-header__fix-configuration"
>
<svg
width="5"
height="6"
viewBox="0 0 5 6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="2.5" cy="3" r="2.5" fill="#FF5A44" />
</svg>
{{ $t('widget.fixConfiguration') }}
</div>
</div>
<div v-if="widget.description" class="widget-header__description">
{{ widget.description }}
</div>
</div>
<WidgetContextMenu
v-if="isEditMode"
:widget="widget"
:dashboard="dashboard"
@delete-widget="$emit('delete-widget', $event)"
></WidgetContextMenu>
</div>
<div
class="dashboard-summary-widget__summary"
:class="{
'dashboard-summary-widget__summary--misconfigured':
dataSourceMisconfigured,
}"
>
{{ result }}
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import WidgetContextMenu from '@baserow/modules/dashboard/components/widget/WidgetContextMenu'
export default {
name: 'SummaryWidget',
components: { WidgetContextMenu },
props: {
dashboard: {
type: Object,
required: true,
},
widget: {
type: Object,
required: true,
},
},
computed: {
...mapGetters({
getDataSourceById: 'dashboardApplication/getDataSourceById',
getDataForDataSource: 'dashboardApplication/getDataForDataSource',
isEditMode: 'dashboardApplication/isEditMode',
}),
dataSource() {
return this.getDataSourceById(this.widget.data_source_id)
},
result() {
const data = this.getDataForDataSource(this.dataSource?.id)
if (data && data.result !== undefined) {
return data.result
}
return 0
},
dataSourceMisconfigured() {
const data = this.getDataForDataSource(this.dataSource?.id)
if (data) {
return !!data._error
}
return false
},
},
}
</script>

View file

@ -0,0 +1,70 @@
<template>
<AggregateRowsDataSourceForm
v-if="dataSource"
ref="dataSourceForm"
:dashboard="dashboard"
:widget="widget"
:data-source="dataSource"
:default-values="dataSource"
@values-changed="onDataSourceValuesChanged"
/>
</template>
<script>
import AggregateRowsDataSourceForm from '@baserow/modules/dashboard/components/data_source/AggregateRowsDataSourceForm'
import error from '@baserow/modules/core/mixins/error'
import { mapActions } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
name: 'SummaryWidgetSettings',
components: { AggregateRowsDataSourceForm },
mixins: [error],
props: {
dashboard: {
type: Object,
required: true,
},
widget: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
}
},
computed: {
dataSource() {
return this.$store.getters['dashboardApplication/getDataSourceById'](
this.widget.data_source_id
)
},
integration() {
return this.$store.getters['dashboardApplication/getIntegrationById'](
this.dataSource.integration_id
)
},
},
methods: {
...mapActions({
updateDataSource: 'dashboardApplication/updateDataSource',
}),
async onDataSourceValuesChanged(changedDataSourceValues) {
if (this.$refs.dataSourceForm.isFormValid()) {
try {
await this.updateDataSource({
dataSourceId: this.dataSource.id,
values: changedDataSourceValues,
})
} catch (error) {
this.$refs.dataSourceForm.reset()
this.$refs.dataSourceForm.touch()
notifyIf(error, 'dashboard')
}
}
},
},
}
</script>

View file

@ -0,0 +1,65 @@
<template>
<Context ref="context" overflow-scroll max-height-if-outside-viewport>
<ul class="context__menu">
<li v-if="canBeDeleted" class="context__menu-item">
<a
class="context__menu-item-link context__menu-item-link--delete"
:class="{ 'context__menu-item-link--loading': isDeleteInProgress }"
@click="deleteWidget()"
>
<i class="context__menu-item-icon iconoir-bin"></i>
{{ $t('widgetContext.delete') }}
</a>
</li>
</ul>
</Context>
</template>
<script>
import context from '@baserow/modules/core/mixins/context'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
name: 'WidgetContext',
mixins: [context],
props: {
dashboard: {
type: Object,
required: true,
},
widget: {
type: Object,
required: true,
},
},
data() {
return {
isDeleteInProgress: false,
}
},
computed: {
canBeDeleted() {
return this.$hasPermission(
'dashboard.widget.delete',
this.widget,
this.dashboard.workspace.id
)
},
},
methods: {
async deleteWidget() {
this.isDeleteInProgress = true
try {
await this.$store.dispatch(
'dashboardApplication/deleteWidget',
this.widget.id
)
} catch (error) {
notifyIf(error, 'dashboard')
}
this.isDeleteInProgress = false
this.hide()
},
},
}
</script>

View file

@ -0,0 +1,37 @@
<template>
<div ref="contextButton" class="widget-header__context-menu">
<ButtonIcon
icon="iconoir-more-vert"
type="secondary"
size="regular"
@click.stop="
$refs.context.toggle($refs.contextButton, 'bottom', 'right', 8, 0)
"
></ButtonIcon>
<WidgetContext
ref="context"
:widget="widget"
:dashboard="dashboard"
@delete-widget="$emit('delete-widget', $event)"
></WidgetContext>
</div>
</template>
<script>
import WidgetContext from '@baserow/modules/dashboard/components/widget/WidgetContext'
export default {
name: 'WidgetContextMenu',
components: { WidgetContext },
props: {
dashboard: {
type: Object,
required: true,
},
widget: {
type: Object,
required: true,
},
},
}
</script>

View file

@ -0,0 +1,74 @@
<template>
<div>
<WidgetSettingsBaseForm
ref="form"
:widget="widget"
:default-values="widget"
@values-changed="baseFormValuesChanged($event)"
/>
<component
:is="widgetSettingsComponent"
:dashboard="dashboard"
:widget="widget"
/>
</div>
</template>
<script>
import _ from 'lodash'
import WidgetSettingsBaseForm from '@baserow/modules/dashboard/components/widget/WidgetSettingsBaseForm'
import { mapActions } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
name: 'WidgetSettings',
components: { WidgetSettingsBaseForm },
props: {
dashboard: {
type: Object,
required: true,
},
widget: {
type: Object,
required: true,
},
},
computed: {
widgetType() {
return this.$registry.get('dashboardWidget', this.widget.type)
},
widgetSettingsComponent() {
return this.widgetType.settingsComponent
},
},
methods: {
...mapActions({
updateWidget: 'dashboardApplication/updateWidget',
}),
async baseFormValuesChanged(values) {
if (this.$refs.form.isFormValid()) {
const defaultValues = this.$refs.form.defaultValues
const originalValues = JSON.parse(JSON.stringify(defaultValues))
const defaultWithUpdatedValues = { ...defaultValues, ...values }
// Compute only the values in the form that has been actually
// changed
const updatedValues = Object.fromEntries(
Object.entries(defaultWithUpdatedValues).filter(
([key, value]) => !_.isEqual(value, defaultValues[key])
)
)
try {
await this.updateWidget({
widgetId: this.widget.id,
values: updatedValues,
originalValues,
})
} catch (error) {
notifyIf(error, 'dashboard')
}
}
},
},
}
</script>

View file

@ -0,0 +1,101 @@
<template>
<form
class="widget-settings-base-form"
@submit.prevent
@keydown.enter.prevent
>
<FormSection>
<FormGroup
small-label
:label="$t('widgetSettings.title')"
:error="fieldHasErrors('title')"
class="margin-bottom-2"
required
>
<FormInput
ref="title"
v-model="values.title"
:placeholder="$t('widgetSettings.title')"
:error="fieldHasErrors('title')"
@blur="$v.values.title.$touch()"
></FormInput>
<template #error>
<span v-if="$v.values.title.$dirty && !$v.values.title.required">
{{ $t('error.requiredField') }}
</span>
<span v-if="$v.values.title.$dirty && !$v.values.title.maxLength">
{{ $t('error.maxLength', { max: 255 }) }}
</span>
</template>
</FormGroup>
<FormGroup
small-label
:label="$t('widgetSettings.description')"
:error="fieldHasErrors('description')"
class="margin-bottom-2"
>
<FormTextarea
ref="description"
v-model="values.description"
:max-rows="2"
:auto-expandable="true"
size="small"
:placeholder="$t('widgetSettings.description') + '...'"
:error="fieldHasErrors('description')"
@blur="$v.values.description.$touch()"
></FormTextarea>
<template #error>
<span
v-if="
$v.values.description.$dirty && !$v.values.description.maxLength
"
>
{{ $t('error.maxLength', { max: 255 }) }}
</span>
</template>
</FormGroup>
</FormSection>
</form>
</template>
<script>
import { required, maxLength } from 'vuelidate/lib/validators'
import form from '@baserow/modules/core/mixins/form'
export default {
name: 'WidgetSettingsBaseForm',
mixins: [form],
props: {
widget: {
type: Object,
required: true,
},
},
data() {
return {
allowedValues: ['title', 'description'],
values: {
title: '',
description: '',
},
emitValuesOnReset: false,
}
},
validations() {
return {
values: {
title: { required, maxLength: maxLength(255) },
description: { maxLength: maxLength(255) },
},
}
},
watch: {
widget: {
handler(value) {
this.reset(true)
},
deep: true,
},
},
}
</script>

View file

@ -5,7 +5,40 @@
"dashboardHeader": {
"doneEditing": "Done editing"
},
"emptyDashboard": {
"title": "This dashboard doesn't have any widgets",
"subtitle": "Get started by adding one.",
"addWidget": "Add widget"
},
"dashboardHeaderMenuItems": {
"editMode": "Edit mode"
},
"createWidgetModal": {
"title": "Add new widget"
},
"widget": {
"fixConfiguration": "Fix configuration"
},
"summaryWidget": {
"name": "Summary"
},
"emptyDashboardSidebar": {
"title": "No element selected",
"message": "Click on one of the elements to see details."
},
"widgetSettings": {
"title": "Title",
"description": "Description"
},
"aggregateRowsDataSourceForm": {
"data": "Data",
"sourceFieldLabel": "Source",
"viewFieldLabel": "View",
"notSelected": "Not selected",
"aggregationFieldLabel": "Field",
"aggregationTypeLabel": "Summary"
},
"widgetContext": {
"delete": "Delete"
}
}

View file

@ -1,3 +1,6 @@
/**
* Nothing here yet
*/
import dashboardLoading from '@baserow/modules/dashboard/middleware/dashboardLoading'
/* eslint-disable-next-line */
import Middleware from './middleware'
Middleware.dashboardLoading = dashboardLoading

View file

@ -0,0 +1,17 @@
/**
* Middleware that changes the dashboard loading state to true before the route
* changes.
*/
export default async function ({ route, from, store, app }) {
function parseIntOrNull(x) {
return x != null ? parseInt(x) : null
}
const toDashboardId = parseIntOrNull(route?.params?.dashboardId)
const fromDashboardId = parseIntOrNull(from?.params?.dashboardId)
const differentDashboardId = fromDashboardId !== toDashboardId
if (!from || differentDashboardId) {
await store.dispatch('dashboardApplication/setLoading', true)
}
}

View file

@ -1,70 +1,24 @@
<template>
<div class="dashboard-app">
<DashboardHeader :dashboard="dashboard" />
<div class="layout__col-2-2 dashboard-app__content">
<div class="dashboard-app__content-header">
<div class="dashboard-app__title">
<Editable
ref="dashboardNameEditable"
:value="dashboard.name"
@change="renameApplication(dashboard, $event)"
@editing="editingDashboardName = $event"
></Editable>
<a
v-if="isEditMode"
class="dashboard-app__edit"
:class="{ 'visibility-hidden': editingDashboardName }"
@click="editName"
>
<i class="dashboard-app__edit-icon iconoir-edit-pencil"></i
></a>
</div>
<div
v-if="isEditMode || dashboard.description"
class="dashboard-app__description"
>
<Editable
ref="dashboardDescriptionEditable"
:value="dashboard.description"
:placeholder="$t('dashboard.descriptionPlaceholder')"
@change="updateDescription(dashboard, $event)"
@editing="editingDashboardDescription = $event"
></Editable>
<a
v-if="isEditMode"
class="dashboard-app__edit"
:class="{ 'visibility-hidden': editingDashboardDescription }"
@click="editDescription"
>
<i class="dashboard-app__edit-icon iconoir-edit-pencil"></i
></a>
</div>
</div>
</div>
<DashboardContent :dashboard="dashboard" />
</div>
</template>
<script>
import DashboardHeader from '@baserow/modules/dashboard/components/DashboardHeader'
import { notifyIf } from '@baserow/modules/core/utils/error'
import { mapGetters } from 'vuex'
import DashboardContent from '@baserow/modules/dashboard/components/DashboardContent'
export default {
name: 'Dashboard',
components: { DashboardHeader },
beforeRouteUpdate(to, from, next) {
if (from.params.dashboardId !== to.params?.dashboardId) {
this.$store.dispatch('dashboardApplication/reset')
}
next()
},
components: { DashboardHeader, DashboardContent },
beforeRouteLeave(to, from, next) {
this.$store.dispatch('dashboardApplication/reset')
this.$store.dispatch('application/unselect')
next()
},
layout: 'app',
async asyncData({ store, params, error, $registry }) {
middleware: 'dashboardLoading',
async asyncData({ app, store, params, error, $registry }) {
const dashboardId = parseInt(params.dashboardId)
const data = {}
try {
@ -76,63 +30,22 @@ export default {
'workspace/selectById',
dashboard.workspace.id
)
const forEditing = app.$hasPermission(
'application.update',
dashboard,
dashboard.workspace.id
)
await store.dispatch('dashboardApplication/fetchInitial', {
dashboardId: dashboard.id,
forEditing,
})
data.workspace = workspace
data.dashboard = dashboard
await store.dispatch('dashboardApplication/setLoading', false)
} catch (e) {
return error({ statusCode: 404, message: 'Dashboard not found.' })
}
return data
},
data() {
return {
editingDashboardName: false,
editingDashboardDescription: false,
}
},
computed: {
...mapGetters({
isEditMode: 'dashboardApplication/isEditMode',
}),
},
methods: {
editName() {
this.$refs.dashboardNameEditable.edit()
this.editingDashboardName = true
},
editDescription() {
this.$refs.dashboardDescriptionEditable.edit()
this.editingDashboardDescription = true
},
async renameApplication(application, event) {
try {
await this.$store.dispatch('application/update', {
application,
values: {
name: event.value,
},
})
} catch (error) {
this.$refs.dashboardNameEditable.set(event.oldValue)
notifyIf(error, 'application')
} finally {
this.editingDashboardName = false
}
},
async updateDescription(application, event) {
try {
await this.$store.dispatch('application/update', {
application,
values: {
description: event.value,
},
})
} catch (error) {
this.$refs.dashboardDescriptionEditable.set(event.oldValue)
notifyIf(error, 'application')
} finally {
this.editingDashboardDescription = false
}
},
},
}
</script>

View file

@ -8,6 +8,7 @@ import pl from '@baserow/modules/dashboard/locales/pl.json'
import ko from '@baserow/modules/dashboard/locales/ko.json'
import { DashboardApplicationType } from '@baserow/modules/dashboard/applicationTypes'
import { SummaryWidgetType } from '@baserow/modules/dashboard/widgetTypes'
import dashboardApplicationStore from '@baserow/modules/dashboard/store/dashboardApplication'
import { FF_DASHBOARDS } from '@baserow/modules/core/plugins/featureFlags'
@ -31,5 +32,6 @@ export default (context) => {
if (app.$featureFlagIsEnabled(FF_DASHBOARDS)) {
app.$registry.register('application', new DashboardApplicationType(context))
app.$registry.register('dashboardWidget', new SummaryWidgetType(context))
}
}

View file

@ -0,0 +1,13 @@
export default (client) => {
return {
getAllDataSources(dashboardId) {
return client.get(`/dashboard/${dashboardId}/data-sources/`)
},
update(dataSourceId, values = {}) {
return client.patch(`/dashboard/data-sources/${dataSourceId}/`, values)
},
dispatch(dataSourceId) {
return client.post(`/dashboard/data-sources/${dataSourceId}/dispatch/`)
},
}
}

View file

@ -0,0 +1,18 @@
export default (client) => {
return {
getAllWidgets(dashboardId) {
return client.get(`/dashboard/${dashboardId}/widgets/`)
},
create(dashboardId, widget = {}) {
return client.post(`/dashboard/${dashboardId}/widgets/`, {
...widget,
})
},
update(widgetId, values = {}) {
return client.patch(`/dashboard/widgets/${widgetId}/`, values)
},
delete(widgetId) {
return client.delete(`/dashboard/widgets/${widgetId}/`)
},
}
}

View file

@ -1,29 +1,228 @@
import WidgetService from '@baserow/modules/dashboard/services/widget'
import DataSourceService from '@baserow/modules/dashboard/services/dataSource'
import IntegrationService from '@baserow/modules/core/services/integration'
import debounce from 'lodash/debounce'
export const state = () => ({
loading: false,
editMode: false,
selectedWidgetId: null,
widgets: [],
dataSources: [],
integrations: [],
// A cache for data that has been
// returned as a result of dispatching
// a data source. The keys are data source ids.
data: {},
})
let debouncedWidgetUpdate = null
export const mutations = {
RESET(state) {
state.editMode = false
state.selectedWidgetId = null
state.widgets = []
state.dataSources = []
state.integrations = []
state.data = {}
},
TOGGLE_EDIT_MODE(state) {
state.editMode = !state.editMode
},
ADD_WIDGET(state, widget) {
state.widgets.push(widget)
},
ADD_DATA_SOURCE(state, dataSource) {
state.dataSources.push(dataSource)
},
UPDATE_DATA_SOURCE(state, { dataSourceId, values }) {
const dataSource = state.dataSources.find(
(dataSource) => dataSource.id === dataSourceId
)
Object.assign(dataSource, values)
},
UPDATE_DATA(state, { dataSourceId, values }) {
if (state.data[dataSourceId] === undefined) {
state.data[dataSourceId] = {}
}
state.data = {
...state.data,
[dataSourceId]: { ...values },
}
},
ADD_INTEGRATION(state, integration) {
state.integrations.push(integration)
},
SELECT_WIDGET(state, widgetId) {
state.selectedWidgetId = widgetId
},
UPDATE_WIDGET(state, { widgetId, values }) {
const widget = state.widgets.find((widget) => widget.id === widgetId)
Object.assign(widget, values)
},
DELETE_WIDGET(state, widgetId) {
const index = state.widgets.findIndex((widget) => widget.id === widgetId)
state.widgets.splice(index, 1)
},
SET_LOADING(state, value) {
state.loading = value
},
}
export const actions = {
setLoading({ commit }, value) {
commit('SET_LOADING', value)
},
reset({ commit }) {
commit('RESET')
},
toggleEditMode({ commit }) {
commit('TOGGLE_EDIT_MODE')
},
enterEditMode({ getters, commit }) {
if (!getters.isEditMode) {
commit('TOGGLE_EDIT_MODE')
}
},
selectWidget({ commit }, widgetId) {
commit('SELECT_WIDGET', widgetId)
},
updateWidget({ commit }, { widgetId, values, originalValues }) {
return new Promise((resolve, reject) => {
commit('UPDATE_WIDGET', { widgetId, values })
let previousOriginalValues = originalValues
if (debouncedWidgetUpdate) {
debouncedWidgetUpdate.cancel()
previousOriginalValues = debouncedWidgetUpdate.originalValues
}
debouncedWidgetUpdate = debounce(async () => {
try {
await WidgetService(this.$client).update(widgetId, values)
debouncedWidgetUpdate = null
resolve()
} catch (error) {
commit('UPDATE_WIDGET', { widgetId, values: previousOriginalValues })
reject(error)
}
}, 1000)
debouncedWidgetUpdate.originalValues = previousOriginalValues
debouncedWidgetUpdate()
})
},
async updateDataSource({ commit, dispatch }, { dataSourceId, values }) {
const { data } = await DataSourceService(this.$client).update(
dataSourceId,
values
)
commit('UPDATE_DATA_SOURCE', { dataSourceId, values: data })
try {
await dispatch('dispatchDataSource', dataSourceId)
} catch (error) {
commit('UPDATE_DATA', { dataSourceId, values: { _error: true } })
}
},
async fetchInitial({ commit, dispatch }, { dashboardId, forEditing }) {
commit('RESET')
const { data } = await WidgetService(this.$client).getAllWidgets(
dashboardId
)
data.forEach((widget) => {
commit('ADD_WIDGET', widget)
})
await dispatch('fetchNewDataSources', dashboardId)
if (forEditing) {
const { data: integrationsData } = await IntegrationService(
this.$client
).fetchAll(dashboardId)
integrationsData.forEach((integration) => {
commit('ADD_INTEGRATION', integration)
})
}
},
async fetchNewDataSources({ commit, dispatch, getters }, dashboardId) {
const { data: dataSourcesData } = await DataSourceService(
this.$client
).getAllDataSources(dashboardId)
dataSourcesData.forEach(async (dataSource) => {
if (!getters.getDataSourceById(dataSource.id)) {
commit('ADD_DATA_SOURCE', dataSource)
await dispatch('dispatchDataSource', dataSource.id)
}
})
},
async createWidget({ dispatch }, { dashboard, widget }) {
const { data } = await WidgetService(this.$client).create(
dashboard.id,
widget
)
return await dispatch('handleNewWidgetCreated', {
...data,
})
},
async handleNewWidgetCreated({ commit, dispatch }, createdWidget) {
commit('ADD_WIDGET', createdWidget)
await dispatch('fetchNewDataSources', createdWidget.dashboard_id)
dispatch('selectWidget', createdWidget.id)
},
async dispatchDataSource({ commit }, dataSourceId) {
try {
const { data } = await DataSourceService(this.$client).dispatch(
dataSourceId
)
commit('UPDATE_DATA', { dataSourceId, values: data })
} catch (error) {
commit('UPDATE_DATA', { dataSourceId, values: { _error: true } })
}
},
async deleteWidget({ commit }, widgetId) {
await WidgetService(this.$client).delete(widgetId)
commit('DELETE_WIDGET', widgetId)
},
}
export const getters = {
isEditMode(state) {
return state.editMode
},
isLoading(state) {
return state.loading
},
isEmpty(state) {
return state.widgets.length === 0
},
getWidgetById: (state, getters) => (widgetId) => {
return state.widgets.find((widget) => widget.id === widgetId)
},
getWidgets(state) {
return state.widgets
},
getSelectedWidgetId(state) {
return state.selectedWidgetId
},
getSelectedWidget(state) {
return state.widgets.find((widget) => widget.id === state.selectedWidgetId)
},
getDataSourceById: (state, getters) => (dataSourceId) => {
return state.dataSources.find(
(dataSource) => dataSource.id === dataSourceId
)
},
getDataForDataSource: (state, getters) => (dataSourceId) => {
return state.data[dataSourceId]
},
getIntegrations(state) {
return state.integrations
},
getIntegrationById: (state) => (integrationId) => {
return state.integrations.find(
(integration) => integration.id === integrationId
)
},
}
export default {

View file

@ -0,0 +1,57 @@
import { Registerable } from '@baserow/modules/core/registry'
import SummaryWidgetSvg from '@baserow/modules/dashboard/assets/images/widgets/summary_widget.svg'
import SummaryWidget from '@baserow/modules/dashboard/components/widget/SummaryWidget'
import SummaryWidgetSettings from '@baserow/modules/dashboard/components/widget/SummaryWidgetSettings'
export class WidgetType extends Registerable {
constructor(...args) {
super(...args)
this.type = this.getType()
if (this.type === null) {
throw new Error('The type name of a widget type must be set.')
}
if (this.name === null) {
throw new Error('The name of a widget type must be set.')
}
}
get name() {
return null
}
get createWidgetImage() {
return null
}
get component() {
return null
}
get settingsComponent() {
return null
}
}
export class SummaryWidgetType extends WidgetType {
static getType() {
return 'summary'
}
get name() {
return this.app.i18n.t('summaryWidget.name')
}
get createWidgetImage() {
return SummaryWidgetSvg
}
get component() {
return SummaryWidget
}
get settingsComponent() {
return SummaryWidgetSettings
}
}

View file

@ -48,7 +48,7 @@ exports[`Dropdown component Test slots 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -119,7 +119,7 @@ exports[`Dropdown component With items 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -205,7 +205,7 @@ exports[`Dropdown component With items 2`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -236,7 +236,7 @@ exports[`Dropdown component With items 2`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -267,7 +267,7 @@ exports[`Dropdown component With items 2`] = `
</li>
<li
class="select__item select__item--no-options disabled"
class="select__item select__item--no-options visible disabled"
>
<a
class="select__item-link"
@ -298,7 +298,7 @@ exports[`Dropdown component With items 2`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -331,7 +331,7 @@ exports[`Dropdown component With items 2`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -405,7 +405,7 @@ exports[`Dropdown component With items 3`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -436,7 +436,7 @@ exports[`Dropdown component With items 3`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -677,7 +677,7 @@ exports[`Dropdown component change value prop 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -708,7 +708,7 @@ exports[`Dropdown component change value prop 1`] = `
</li>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -794,7 +794,7 @@ exports[`Dropdown component test focus 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active hover"
class="select__item select__item--no-options visible active hover"
>
<a
class="select__item-link"
@ -880,7 +880,7 @@ exports[`Dropdown component test focus 2`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active hover"
class="select__item select__item--no-options visible active hover"
>
<a
class="select__item-link"
@ -966,7 +966,7 @@ exports[`Dropdown component test interactions 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active hover"
class="select__item select__item--no-options visible active hover"
>
<a
class="select__item-link"
@ -997,7 +997,7 @@ exports[`Dropdown component test interactions 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1083,7 +1083,7 @@ exports[`Dropdown component test interactions 2`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active hover"
class="select__item select__item--no-options visible active hover"
>
<a
class="select__item-link"
@ -1114,7 +1114,7 @@ exports[`Dropdown component test interactions 2`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"

View file

@ -230,7 +230,7 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -261,7 +261,7 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -292,7 +292,7 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"

View file

@ -103,7 +103,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active hover"
class="select__item select__item--no-options visible active hover"
>
<a
class="select__item-link"
@ -134,7 +134,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -331,7 +331,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -362,7 +362,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -393,7 +393,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -424,7 +424,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -455,7 +455,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -486,7 +486,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -611,7 +611,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -642,7 +642,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -673,7 +673,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -704,7 +704,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -735,7 +735,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -766,7 +766,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -797,7 +797,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -828,7 +828,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -859,7 +859,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -890,7 +890,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -921,7 +921,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -952,7 +952,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -983,7 +983,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1014,7 +1014,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1045,7 +1045,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1076,7 +1076,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1107,7 +1107,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1138,7 +1138,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1169,7 +1169,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1200,7 +1200,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1231,7 +1231,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1262,7 +1262,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1293,7 +1293,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1324,7 +1324,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1355,7 +1355,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1386,7 +1386,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1417,7 +1417,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1448,7 +1448,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1479,7 +1479,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1510,7 +1510,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1541,7 +1541,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1572,7 +1572,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1603,7 +1603,7 @@ exports[`Preview exportTableModal Modal with no view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1907,7 +1907,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options hover"
class="select__item select__item--no-options visible hover"
>
<a
class="select__item-link"
@ -1938,7 +1938,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -2135,7 +2135,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -2166,7 +2166,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2197,7 +2197,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2228,7 +2228,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2259,7 +2259,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2290,7 +2290,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2415,7 +2415,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -2446,7 +2446,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2477,7 +2477,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2508,7 +2508,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2539,7 +2539,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2570,7 +2570,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2601,7 +2601,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2632,7 +2632,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2663,7 +2663,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2694,7 +2694,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2725,7 +2725,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2756,7 +2756,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2787,7 +2787,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2818,7 +2818,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2849,7 +2849,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2880,7 +2880,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2911,7 +2911,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2942,7 +2942,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2973,7 +2973,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3004,7 +3004,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3035,7 +3035,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3066,7 +3066,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3097,7 +3097,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3128,7 +3128,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3159,7 +3159,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3190,7 +3190,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3221,7 +3221,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3252,7 +3252,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3283,7 +3283,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3314,7 +3314,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3345,7 +3345,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3376,7 +3376,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -3407,7 +3407,7 @@ exports[`Preview exportTableModal Modal with view 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"

View file

@ -150,7 +150,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -180,7 +180,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -210,7 +210,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -298,7 +298,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -328,7 +328,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -358,7 +358,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -388,7 +388,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -418,7 +418,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -448,7 +448,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -478,7 +478,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -508,7 +508,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -538,7 +538,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -659,7 +659,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -690,7 +690,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -783,7 +783,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -813,7 +813,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -843,7 +843,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -931,7 +931,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -961,7 +961,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -991,7 +991,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1021,7 +1021,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1051,7 +1051,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1081,7 +1081,7 @@ exports[`ViewFilterForm match snapshots Full view filter component 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1300,7 +1300,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1330,7 +1330,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
/>
</li>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -1360,7 +1360,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1449,7 +1449,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active hover"
class="select__item select__item--no-options visible active hover"
>
<a
class="select__item-link"
@ -1479,7 +1479,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1509,7 +1509,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1539,7 +1539,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1569,7 +1569,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1599,7 +1599,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 1`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1818,7 +1818,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1848,7 +1848,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
/>
</li>
<li
class="select__item select__item--no-options active"
class="select__item select__item--no-options visible active"
>
<a
class="select__item-link"
@ -1878,7 +1878,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -1967,7 +1967,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
tabindex="-1"
>
<li
class="select__item select__item--no-options active hover"
class="select__item select__item--no-options visible active hover"
>
<a
class="select__item-link"
@ -1997,7 +1997,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2027,7 +2027,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2057,7 +2057,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2087,7 +2087,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"
@ -2117,7 +2117,7 @@ exports[`ViewFilterForm match snapshots Test rating filter 2`] = `
/>
</li>
<li
class="select__item select__item--no-options"
class="select__item select__item--no-options visible"
>
<a
class="select__item-link"