mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 09:08:32 +00:00
Resolve "Chart widget: series configuration"
This commit is contained in:
parent
5c2511da13
commit
b8ce60759e
12 changed files with 486 additions and 31 deletions
enterprise
backend/src/baserow_enterprise
api/integrations/local_baserow
integrations/local_baserow
migrations
web-frontend/modules/baserow_enterprise
assets/scss/components
dashboard/components/data_source
locales
web-frontend/modules/core
|
@ -7,7 +7,7 @@ from baserow_enterprise.integrations.local_baserow.models import (
|
||||||
|
|
||||||
|
|
||||||
class LocalBaserowTableServiceAggregationSeriesSerializer(serializers.ModelSerializer):
|
class LocalBaserowTableServiceAggregationSeriesSerializer(serializers.ModelSerializer):
|
||||||
field_id = serializers.IntegerField()
|
field_id = serializers.IntegerField(allow_null=True)
|
||||||
order = serializers.IntegerField(read_only=True)
|
order = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -83,6 +83,7 @@ class LocalBaserowTableServiceAggregationSeries(models.Model):
|
||||||
field = models.ForeignKey(
|
field = models.ForeignKey(
|
||||||
"database.Field",
|
"database.Field",
|
||||||
help_text="The aggregated field.",
|
help_text="The aggregated field.",
|
||||||
|
null=True,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
aggregation_type = models.CharField(
|
aggregation_type = models.CharField(
|
||||||
|
|
|
@ -130,36 +130,39 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
|
||||||
table_field_ids = [field.id for field in table_fields]
|
table_field_ids = [field.id for field in table_fields]
|
||||||
|
|
||||||
def validate_agg_series(agg_series):
|
def validate_agg_series(agg_series):
|
||||||
if agg_series["field_id"] not in table_field_ids:
|
if agg_series["aggregation_type"]:
|
||||||
raise DRFValidationError(
|
try:
|
||||||
detail=f"The field with ID {agg_series['field_id']} is not "
|
agg_type = field_aggregation_registry.get(
|
||||||
"related to the given table.",
|
agg_series["aggregation_type"]
|
||||||
code="invalid_field",
|
)
|
||||||
)
|
except AggregationTypeDoesNotExist:
|
||||||
|
raise DRFValidationError(
|
||||||
|
detail=f"The aggregation type '{agg_series['aggregation_type']}' "
|
||||||
|
f"doesn't exist",
|
||||||
|
code="invalid_aggregation_raw_type",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
if agg_series["field_id"] is not None:
|
||||||
agg_type = field_aggregation_registry.get(
|
if agg_series["field_id"] not in table_field_ids:
|
||||||
agg_series["aggregation_type"]
|
raise DRFValidationError(
|
||||||
)
|
detail=f"The field with ID {agg_series['field_id']} is not "
|
||||||
except AggregationTypeDoesNotExist:
|
"related to the given table.",
|
||||||
raise DRFValidationError(
|
code="invalid_field",
|
||||||
detail=f"The aggregation type '{agg_series['aggregation_type']}' "
|
)
|
||||||
f"doesn't exist",
|
|
||||||
code="invalid_aggregation_raw_type",
|
field = next(
|
||||||
)
|
(
|
||||||
field = next(
|
field
|
||||||
(
|
for field in table_fields
|
||||||
field
|
if field.id == agg_series["field_id"]
|
||||||
for field in table_fields
|
)
|
||||||
if field.id == agg_series["field_id"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not agg_type.field_is_compatible(field):
|
|
||||||
raise DRFValidationError(
|
|
||||||
detail=f"The field with ID {agg_series['field_id']} is not compatible "
|
|
||||||
f"with aggregation type {agg_series['aggregation_type']}.",
|
|
||||||
code="invalid_aggregation_raw_type",
|
|
||||||
)
|
)
|
||||||
|
if not agg_type.field_is_compatible(field):
|
||||||
|
raise DRFValidationError(
|
||||||
|
detail=f"The field with ID {agg_series['field_id']} is not compatible "
|
||||||
|
f"with aggregation type {agg_series['aggregation_type']}.",
|
||||||
|
code="invalid_aggregation_raw_type",
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -446,6 +449,10 @@ class LocalBaserowGroupedAggregateRowsUserServiceType(
|
||||||
f"There are no aggregation series defined."
|
f"There are no aggregation series defined."
|
||||||
)
|
)
|
||||||
for agg_series in defined_agg_series:
|
for agg_series in defined_agg_series:
|
||||||
|
if agg_series.field is None:
|
||||||
|
raise ServiceImproperlyConfigured(
|
||||||
|
f"The aggregation series field has to be set."
|
||||||
|
)
|
||||||
if agg_series.field.trashed:
|
if agg_series.field.trashed:
|
||||||
raise ServiceImproperlyConfigured(
|
raise ServiceImproperlyConfigured(
|
||||||
f"The field with ID {agg_series.field.id} is trashed."
|
f"The field with ID {agg_series.field.id} is trashed."
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 5.0.9 on 2025-02-11 10:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("baserow_enterprise", "0040_chartwidget"),
|
||||||
|
("database", "0180_view_allow_public_export"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="localbaserowtableserviceaggregationseries",
|
||||||
|
name="field",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="The aggregated field.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="database.field",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,6 @@
|
||||||
|
.aggregation-series-form {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: $palette-neutral-50;
|
||||||
|
padding: 16px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
|
@ -19,3 +19,4 @@
|
||||||
@import 'saml_auth_link';
|
@import 'saml_auth_link';
|
||||||
@import 'oidc_auth_link';
|
@import 'oidc_auth_link';
|
||||||
@import 'dashboard_chart_widget';
|
@import 'dashboard_chart_widget';
|
||||||
|
@import 'aggregation_series_form';
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
<template>
|
||||||
|
<div class="aggregation-series-form">
|
||||||
|
<FormSection>
|
||||||
|
<FormGroup
|
||||||
|
small-label
|
||||||
|
:label="$t('aggregationSeriesForm.aggregationTypeLabel')"
|
||||||
|
required
|
||||||
|
horizontal
|
||||||
|
horizontal-narrow
|
||||||
|
class="margin-bottom-2"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<FormGroup
|
||||||
|
small-label
|
||||||
|
:label="$t('aggregationSeriesForm.aggregationFieldLabel')"
|
||||||
|
required
|
||||||
|
horizontal
|
||||||
|
horizontal-narrow
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
v-model="values.field_id"
|
||||||
|
:error="fieldHasErrors('field_id')"
|
||||||
|
:disabled="compatibleFields.length === 0"
|
||||||
|
@change="$v.values.field_id.$touch()"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
v-for="field in compatibleFields"
|
||||||
|
:key="field.id"
|
||||||
|
:name="field.name"
|
||||||
|
:value="field.id"
|
||||||
|
:icon="fieldIconClass(field)"
|
||||||
|
>
|
||||||
|
</DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
</FormGroup>
|
||||||
|
</FormSection>
|
||||||
|
<FormSection>
|
||||||
|
<ButtonText
|
||||||
|
icon="iconoir-bin"
|
||||||
|
type="secondary"
|
||||||
|
@click="$emit('delete-series', seriesIndex)"
|
||||||
|
>{{ $t('aggregationSeriesForm.deleteSeries') }}</ButtonText
|
||||||
|
>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import form from '@baserow/modules/core/mixins/form'
|
||||||
|
import { required } from 'vuelidate/lib/validators'
|
||||||
|
|
||||||
|
const includes = (array) => (value) => {
|
||||||
|
return array.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AggregationSeriesForm',
|
||||||
|
mixins: [form],
|
||||||
|
props: {
|
||||||
|
tableFields: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
seriesIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allowedValues: ['field_id', 'aggregation_type'],
|
||||||
|
values: {
|
||||||
|
field_id: null,
|
||||||
|
aggregation_type: null,
|
||||||
|
},
|
||||||
|
emitValuesOnReset: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
viewAggregationTypes() {
|
||||||
|
return this.$registry.getOrderedList('viewAggregation')
|
||||||
|
},
|
||||||
|
aggregationTypeNames() {
|
||||||
|
return this.viewAggregationTypes.map((aggType) => aggType.getType())
|
||||||
|
},
|
||||||
|
compatibleFields() {
|
||||||
|
if (!this.values.aggregation_type) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const aggType = this.$registry.get(
|
||||||
|
'viewAggregation',
|
||||||
|
this.values.aggregation_type
|
||||||
|
)
|
||||||
|
return this.tableFields.filter((tableField) =>
|
||||||
|
aggType.fieldIsCompatible(tableField)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
compatibleTableFieldIds() {
|
||||||
|
return this.compatibleFields.map((field) => field.id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
defaultValues: {
|
||||||
|
handler() {
|
||||||
|
this.reset(true)
|
||||||
|
this.$v.$touch(true)
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
'values.aggregation_type': {
|
||||||
|
handler(aggregationType) {
|
||||||
|
if (
|
||||||
|
aggregationType !== null &&
|
||||||
|
aggregationType !== this.defaultValues.aggregation_type &&
|
||||||
|
this.values.field_id !== null
|
||||||
|
) {
|
||||||
|
// If both the field and aggregation type
|
||||||
|
// are selected, check if they are still
|
||||||
|
// compatible.
|
||||||
|
const aggType = this.$registry.get('viewAggregation', aggregationType)
|
||||||
|
const field = this.tableFields.filter(
|
||||||
|
(field) => field.id === this.values.field_id
|
||||||
|
)
|
||||||
|
if (!aggType.fieldIsCompatible(field)) {
|
||||||
|
this.values.field_id = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validations() {
|
||||||
|
return {
|
||||||
|
values: {
|
||||||
|
aggregation_type: {
|
||||||
|
required,
|
||||||
|
isValidAggregationType: includes(this.aggregationTypeNames),
|
||||||
|
},
|
||||||
|
field_id: {
|
||||||
|
required,
|
||||||
|
isValidFieldId: includes(this.compatibleTableFieldIds),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fieldIconClass(field) {
|
||||||
|
const fieldType = this.$registry.get('field', field.type)
|
||||||
|
return fieldType.iconClass
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,10 +1,115 @@
|
||||||
<template><div></div></template>
|
<template>
|
||||||
|
<form @submit.prevent>
|
||||||
|
<FormSection
|
||||||
|
:title="$t('groupedAggregateRowsDataSourceForm.data')"
|
||||||
|
class="margin-bottom-2"
|
||||||
|
>
|
||||||
|
<FormGroup
|
||||||
|
:label="$t('groupedAggregateRowsDataSourceForm.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('groupedAggregateRowsDataSourceForm.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('groupedAggregateRowsDataSourceForm.notSelected')"
|
||||||
|
:value="null"
|
||||||
|
>{{
|
||||||
|
$t('groupedAggregateRowsDataSourceForm.notSelected')
|
||||||
|
}}</DropdownItem
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
v-for="view in tableViews"
|
||||||
|
:key="view.id"
|
||||||
|
:name="view.name"
|
||||||
|
:value="view.id"
|
||||||
|
>
|
||||||
|
{{ view.name }}
|
||||||
|
</DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
</FormGroup>
|
||||||
|
</FormSection>
|
||||||
|
<FormSection
|
||||||
|
v-if="values.table_id && !fieldHasErrors('table_id')"
|
||||||
|
:title="$t('groupedAggregateRowsDataSourceForm.series')"
|
||||||
|
class="margin-bottom-2"
|
||||||
|
>
|
||||||
|
<template #title-slot>
|
||||||
|
<ButtonText icon="iconoir-plus" type="secondary" @click="addSeries">{{
|
||||||
|
$t('groupedAggregateRowsDataSourceForm.addSeries')
|
||||||
|
}}</ButtonText>
|
||||||
|
</template>
|
||||||
|
<div class="margin-bottom-2"></div>
|
||||||
|
<AggregationSeriesForm
|
||||||
|
v-for="(series, index) in values.aggregation_series"
|
||||||
|
:key="index"
|
||||||
|
:table-fields="tableFields"
|
||||||
|
:series-index="index"
|
||||||
|
:default-values="series"
|
||||||
|
@delete-series="deleteSeries"
|
||||||
|
@values-changed="onAggregationSeriesUpdated(index, $event)"
|
||||||
|
>
|
||||||
|
</AggregationSeriesForm>
|
||||||
|
</FormSection>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import form from '@baserow/modules/core/mixins/form'
|
import form from '@baserow/modules/core/mixins/form'
|
||||||
|
import { required } from 'vuelidate/lib/validators'
|
||||||
|
import AggregationSeriesForm from '@baserow_enterprise/dashboard/components/data_source/AggregationSeriesForm'
|
||||||
|
|
||||||
|
const includesIfSet = (array) => (value) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return array.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GroupedAggregateRowsDataSourceForm',
|
name: 'GroupedAggregateRowsDataSourceForm',
|
||||||
|
components: { AggregationSeriesForm },
|
||||||
mixins: [form],
|
mixins: [form],
|
||||||
props: {
|
props: {
|
||||||
dashboard: {
|
dashboard: {
|
||||||
|
@ -25,5 +130,124 @@ export default {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allowedValues: ['table_id', 'view_id', 'aggregation_series'],
|
||||||
|
values: {
|
||||||
|
table_id: null,
|
||||||
|
view_id: null,
|
||||||
|
aggregation_series: [],
|
||||||
|
},
|
||||||
|
tableLoading: false,
|
||||||
|
databaseSelectedId: null,
|
||||||
|
emitValuesOnReset: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
integration() {
|
||||||
|
return this.$store.getters[
|
||||||
|
`${this.storePrefix}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)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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(true)
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
deep: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validations() {
|
||||||
|
return {
|
||||||
|
values: {
|
||||||
|
table_id: { required, isValidTableId: includesIfSet(this.tableIds) },
|
||||||
|
view_id: { isValidViewId: includesIfSet(this.tableViewIds) },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addSeries() {
|
||||||
|
this.values.aggregation_series.push({
|
||||||
|
field_id: null,
|
||||||
|
aggregation_type: '',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteSeries(index) {
|
||||||
|
this.values.aggregation_series.splice(index, 1)
|
||||||
|
},
|
||||||
|
onAggregationSeriesUpdated(index, aggregationSeriesValues) {
|
||||||
|
const updatedAggregationSeries = this.values.aggregation_series
|
||||||
|
updatedAggregationSeries[index] = aggregationSeriesValues
|
||||||
|
this.$emit('values-changed', {
|
||||||
|
aggregation_series: updatedAggregationSeries,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -442,5 +442,18 @@
|
||||||
},
|
},
|
||||||
"chartWidget": {
|
"chartWidget": {
|
||||||
"name": "Chart"
|
"name": "Chart"
|
||||||
|
},
|
||||||
|
"groupedAggregateRowsDataSourceForm": {
|
||||||
|
"series": "Series",
|
||||||
|
"addSeries": "Add series",
|
||||||
|
"data": "Data",
|
||||||
|
"sourceFieldLabel": "Source",
|
||||||
|
"viewFieldLabel": "View",
|
||||||
|
"notSelected": "Not selected"
|
||||||
|
},
|
||||||
|
"aggregationSeriesForm": {
|
||||||
|
"aggregationFieldLabel": "Field",
|
||||||
|
"aggregationTypeLabel": "Summary type",
|
||||||
|
"deleteSeries": "Delete series"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 16px 16px 14px;
|
padding: 16px 16px 14px;
|
||||||
border-left: 1px solid $palette-neutral-200;
|
border-left: 1px solid $palette-neutral-200;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,3 +236,9 @@
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-section__title-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3 v-if="title" class="form-section__title">{{ title }}</h3>
|
<div class="form-section__title-container">
|
||||||
|
<h3 v-if="title" class="form-section__title">{{ title }}</h3>
|
||||||
|
<div>
|
||||||
|
<slot name="title-slot"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Add table
Reference in a new issue