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

Enhance dashboard charts design

This commit is contained in:
Jonathan Adeline 2025-03-10 05:50:54 +00:00
parent 1dc9a16030
commit f113354cb8
11 changed files with 279 additions and 138 deletions
enterprise/web-frontend/modules/baserow_enterprise
assets/scss/components
dashboard/components/widget
web-frontend/modules

View file

@ -1,3 +1,25 @@
.chart { .chart {
max-height: 320px; max-height: 320px;
} }
.chart__no-data {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 10px 0;
flex-direction: column;
gap: 36px;
}
.chart__no-data-dashed-line {
border-top: 1px dashed $palette-neutral-200;
width: 100%;
height: 1px;
}
.chart__no-data-plain-line {
border-top: 1px solid $palette-neutral-200;
width: 100%;
height: 1px;
}

View file

@ -1,3 +1,15 @@
.dashboard-chart-widget { .dashboard-chart-widget {
padding: 0 24px 24px; // nothing
}
.dashboard-chart-widget__content {
height: 280px;
}
.dashboard-chart-widget__loading {
height: 341px;
.dashboard-chart-widget--with-header-description & {
height: 362.8px;
}
} }

View file

@ -1,5 +1,20 @@
<template> <template>
<Bar id="chart-id" :options="chartOptions" :data="chartData" class="chart" /> <Bar
v-if="chartData.datasets.length > 0"
id="chart-id"
:options="chartOptions"
:data="chartData"
class="chart"
/>
<div v-else class="chart__no-data">
<span class="chart__no-data-dashed-line"></span>
<span class="chart__no-data-dashed-line"></span>
<span class="chart__no-data-dashed-line"></span>
<span class="chart__no-data-dashed-line"></span>
<span class="chart__no-data-dashed-line"></span>
<span class="chart__no-data-plain-line"></span>
</div>
</template> </template>
<script> <script>
@ -50,6 +65,36 @@ export default {
display: true, display: true,
align: 'start', align: 'start',
position: 'bottom', position: 'bottom',
labels: {
usePointStyle: true,
boxWidth: 14,
pointStyle: 'circle',
padding: 20,
},
},
tooltip: {
backgroundColor: '#202128',
padding: 10,
bodyFont: {
size: 12,
},
titleFont: {
size: 12,
},
},
},
elements: {
bar: {
borderRadius: {
topLeft: 4,
topRight: 4,
bottomLeft: 0,
bottomRight: 0,
},
borderWidth: 1,
borderColor: '#5190ef',
backgroundColor: '#5190ef',
hoverBackgroundColor: '#5190ef',
}, },
}, },
} }
@ -88,7 +133,7 @@ export default {
datasets.push({ datasets.push({
data: seriesData, data: seriesData,
label, label,
backgroundColor: this.chartColors[index], ...this.chartColors[index],
}) })
} }
return { return {
@ -107,7 +152,7 @@ export default {
datasets.push({ datasets.push({
data: seriesData, data: seriesData,
label, label,
backgroundColor: this.chartColors[index], ...this.chartColors[index],
}) })
} }
return { return {
@ -124,7 +169,33 @@ export default {
}) })
}, },
chartColors() { chartColors() {
return ['#4E5CFE', '#2BC3F1', '#FFC744', '#E26AB0', '#3E4ACB'] return [
{
backgroundColor: '#5190ef',
borderColor: '#5190ef',
hoverBackgroundColor: '#5190ef',
},
{
backgroundColor: '#2BC3F1',
borderColor: '#2BC3F1',
hoverBackgroundColor: '#2BC3F1',
},
{
backgroundColor: '#FFC744',
borderColor: '#FFC744',
hoverBackgroundColor: '#FFC744',
},
{
backgroundColor: '#E26AB0',
borderColor: '#E26AB0',
hoverBackgroundColor: '#E26AB0',
},
{
backgroundColor: '#3E4ACB',
borderColor: '#3E4ACB',
hoverBackgroundColor: '#3E4ACB',
},
]
}, },
}, },
methods: { methods: {

View file

@ -1,38 +1,48 @@
<template> <template>
<div class="dashboard-chart-widget"> <div
<div class="widget-header"> class="dashboard-chart-widget"
<div class="widget-header__main"> :class="{
<div class="widget-header__title-wrapper"> 'dashboard-chart-widget--with-header-description': widget.description,
<div class="widget-header__title">{{ widget.title }}</div> }"
<div >
v-if="dataSourceMisconfigured" <template v-if="!loading">
class="widget-header__fix-configuration" <div
> class="widget__header"
<svg :class="{
width="5" 'widget__header--edit-mode': editMode,
height="6" }"
viewBox="0 0 5 6" >
fill="none" <div class="widget__header-main">
xmlns="http://www.w3.org/2000/svg" <div class="widget__header-title-wrapper">
<div class="widget__header-title">{{ widget.title }}</div>
<Badge
v-if="dataSourceMisconfigured"
color="red"
size="small"
indicator
rounded
>{{ $t('widget.fixConfiguration') }}</Badge
> >
<circle cx="2.5" cy="3" r="2.5" fill="#FF5A44" /> </div>
</svg> <div v-if="widget.description" class="widget__header-description">
{{ $t('widget.fixConfiguration') }} {{ widget.description }}
</div> </div>
</div> </div>
<div v-if="widget.description" class="widget-header__description"> <WidgetContextMenu
{{ widget.description }} v-if="isEditMode"
</div> :widget="widget"
:dashboard="dashboard"
@delete-widget="$emit('delete-widget', $event)"
></WidgetContextMenu>
</div> </div>
<WidgetContextMenu
v-if="isEditMode" <div class="dashboard-chart-widget__content widget__content">
:widget="widget" <Chart :data-source="dataSource" :data-source-data="dataForDataSource">
:dashboard="dashboard" </Chart>
@delete-widget="$emit('delete-widget', $event)" </div>
></WidgetContextMenu> </template>
</div> <div v-else class="dashboard-chart-widget__loading loading-spinner"></div>
<Chart :data-source="dataSource" :data-source-data="dataForDataSource">
</Chart>
</div> </div>
</template> </template>
@ -57,6 +67,16 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
editMode: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
dataSource() { dataSource() {

View file

@ -7,6 +7,6 @@
@import 'dashboard_widget'; @import 'dashboard_widget';
@import 'dashboard_summary_widget'; @import 'dashboard_summary_widget';
@import 'create_widget_button'; @import 'create_widget_button';
@import 'widget_header'; @import 'widget';
@import 'widget_settings_base_form'; @import 'widget_settings_base_form';
@import 'create_widget_modal'; @import 'create_widget_modal';

View file

@ -1,5 +1,5 @@
.dashboard-summary-widget { .dashboard-summary-widget {
padding: 0 24px 24px; // nothing
} }
.dashboard-summary-widget__summary { .dashboard-summary-widget__summary {
@ -7,7 +7,6 @@
font-size: 40px; font-size: 40px;
font-weight: 600; font-weight: 600;
line-height: 40px; line-height: 40px;
margin-top: 16px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -16,3 +15,11 @@
.dashboard-summary-widget__summary--misconfigured { .dashboard-summary-widget__summary--misconfigured {
color: #cdcecf; color: #cdcecf;
} }
.dashboard-summary-widget__loading {
height: 120px;
.dashboard-summary-widget--with-header-description & {
height: 141.8px;
}
}

View file

@ -0,0 +1,62 @@
.widget__header {
display: flex;
flex-direction: row;
align-items: start;
gap: 7px;
padding: 20px 24px;
border-bottom: 1px solid $palette-neutral-200;
position: relative;
height: auto;
&--no-border {
border-bottom: none;
padding-bottom: 0;
}
&--edit-mode {
padding: 20px 55px 20px 24px;
}
}
.widget__header-main {
display: flex;
flex-direction: column;
min-width: 0;
}
.widget__header-context-menu {
margin-left: auto;
width: 46px;
position: absolute;
right: 0;
top: 8px;
}
.widget__header-title-wrapper {
display: flex;
align-items: center;
gap: 14px;
height: 20px;
}
.widget__header-title {
color: $palette-neutral-1200;
font-size: 16px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.widget__header-description {
@extend %ellipsis;
color: #6a6b70;
font-size: 12px;
font-weight: 400;
margin-top: 8px;
}
.widget__content {
padding: 20px;
}

View file

@ -1,57 +0,0 @@
.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

@ -12,14 +12,12 @@
</div> </div>
<component <component
:is="widgetComponent(widget.type)" :is="widgetComponent(widget.type)"
v-if="isLoading === false"
:dashboard="dashboard" :dashboard="dashboard"
:widget="widget" :widget="widget"
:store-prefix="storePrefix" :store-prefix="storePrefix"
:loading="isLoading"
:edit-mode="isEditMode"
/> />
<div v-else>
<div class="dashboard-widget__loading"></div>
</div>
</div> </div>
</template> </template>

View file

@ -1,45 +1,46 @@
<template> <template>
<div class="dashboard-summary-widget"> <div
<div class="widget-header"> class="dashboard-summary-widget"
<div class="widget-header__main"> :class="{
<div class="widget-header__title-wrapper"> 'dashboard-summary-widget--with-header-description': widget.description,
<div class="widget-header__title">{{ widget.title }}</div> }"
<div >
v-if="dataSourceMisconfigured" <template v-if="!loading">
class="widget-header__fix-configuration" <div class="widget__header widget__header--no-border">
> <div class="widget__header-main">
<svg <div class="widget__header-title-wrapper">
width="5" <div class="widget__header-title">{{ widget.title }}</div>
height="6"
viewBox="0 0 5 6" <Badge
fill="none" v-if="dataSourceMisconfigured"
xmlns="http://www.w3.org/2000/svg" color="red"
indicator
rounded
>{{ $t('widget.fixConfiguration') }}</Badge
> >
<circle cx="2.5" cy="3" r="2.5" fill="#FF5A44" /> </div>
</svg> <div v-if="widget.description" class="widget__header-description">
{{ $t('widget.fixConfiguration') }} {{ widget.description }}
</div> </div>
</div> </div>
<div v-if="widget.description" class="widget-header__description"> <WidgetContextMenu
{{ widget.description }} v-if="isEditMode"
</div> :widget="widget"
:dashboard="dashboard"
@delete-widget="$emit('delete-widget', $event)"
></WidgetContextMenu>
</div> </div>
<WidgetContextMenu <div
v-if="isEditMode" class="widget__content dashboard-summary-widget__summary"
:widget="widget" :class="{
:dashboard="dashboard" 'dashboard-summary-widget__summary--misconfigured':
@delete-widget="$emit('delete-widget', $event)" dataSourceMisconfigured,
></WidgetContextMenu> }"
</div> >
<div {{ result }}
class="dashboard-summary-widget__summary" </div>
:class="{ </template>
'dashboard-summary-widget__summary--misconfigured': <div v-else class="dashboard-summary-widget__loading loading-spinner"></div>
dataSourceMisconfigured,
}"
>
{{ result }}
</div>
</div> </div>
</template> </template>
@ -63,6 +64,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
loading: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
dataSource() { dataSource() {

View file

@ -1,11 +1,11 @@
<template> <template>
<div ref="contextButton" class="widget-header__context-menu"> <div ref="contextButton" class="widget__header-context-menu">
<ButtonIcon <ButtonIcon
icon="iconoir-more-vert" icon="iconoir-more-vert"
type="secondary" type="secondary"
size="regular" size="regular"
@click.stop=" @click.stop="
$refs.context.toggle($refs.contextButton, 'bottom', 'right', 8, 0) $refs.context.toggle($refs.contextButton, 'bottom', 'right', 8, -8)
" "
></ButtonIcon> ></ButtonIcon>
<WidgetContext <WidgetContext