From f113354cb826158366c8ed4ae857118cdcaae77f Mon Sep 17 00:00:00 2001
From: Jonathan Adeline <jonathan@baserow.io>
Date: Mon, 10 Mar 2025 05:50:54 +0000
Subject: [PATCH] Enhance dashboard charts design

---
 .../assets/scss/components/chart.scss         | 22 +++++
 .../components/dashboard_chart_widget.scss    | 14 +++-
 .../dashboard/components/widget/Chart.vue     | 79 +++++++++++++++++-
 .../components/widget/ChartWidget.vue         | 80 ++++++++++++-------
 .../assets/scss/components/dashboard/all.scss |  2 +-
 .../dashboard/dashboard_summary_widget.scss   | 11 ++-
 .../scss/components/dashboard/widget.scss     | 62 ++++++++++++++
 .../components/dashboard/widget_header.scss   | 57 -------------
 .../components/widget/DashboardWidget.vue     |  6 +-
 .../components/widget/SummaryWidget.vue       | 80 ++++++++++---------
 .../components/widget/WidgetContextMenu.vue   |  4 +-
 11 files changed, 279 insertions(+), 138 deletions(-)
 create mode 100644 web-frontend/modules/core/assets/scss/components/dashboard/widget.scss
 delete mode 100644 web-frontend/modules/core/assets/scss/components/dashboard/widget_header.scss

diff --git a/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/chart.scss b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/chart.scss
index 172a17c31..6de5bd78a 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/chart.scss
+++ b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/chart.scss
@@ -1,3 +1,25 @@
 .chart {
   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;
+}
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/dashboard_chart_widget.scss b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/dashboard_chart_widget.scss
index 254e3bd9b..6a3a4a9fb 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/dashboard_chart_widget.scss
+++ b/enterprise/web-frontend/modules/baserow_enterprise/assets/scss/components/dashboard_chart_widget.scss
@@ -1,3 +1,15 @@
 .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;
+  }
 }
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue
index 0c99a92bf..22b194a05 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue
+++ b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/Chart.vue
@@ -1,5 +1,20 @@
 <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>
 
 <script>
@@ -50,6 +65,36 @@ export default {
             display: true,
             align: 'start',
             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({
           data: seriesData,
           label,
-          backgroundColor: this.chartColors[index],
+          ...this.chartColors[index],
         })
       }
       return {
@@ -107,7 +152,7 @@ export default {
         datasets.push({
           data: seriesData,
           label,
-          backgroundColor: this.chartColors[index],
+          ...this.chartColors[index],
         })
       }
       return {
@@ -124,7 +169,33 @@ export default {
       })
     },
     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: {
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/ChartWidget.vue b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/ChartWidget.vue
index 21f26ab60..152c85f44 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/ChartWidget.vue
+++ b/enterprise/web-frontend/modules/baserow_enterprise/dashboard/components/widget/ChartWidget.vue
@@ -1,38 +1,48 @@
 <template>
-  <div class="dashboard-chart-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"
+  <div
+    class="dashboard-chart-widget"
+    :class="{
+      'dashboard-chart-widget--with-header-description': widget.description,
+    }"
+  >
+    <template v-if="!loading">
+      <div
+        class="widget__header"
+        :class="{
+          'widget__header--edit-mode': editMode,
+        }"
+      >
+        <div class="widget__header-main">
+          <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" />
-            </svg>
-            {{ $t('widget.fixConfiguration') }}
+          </div>
+          <div v-if="widget.description" class="widget__header-description">
+            {{ widget.description }}
           </div>
         </div>
-        <div v-if="widget.description" class="widget-header__description">
-          {{ widget.description }}
-        </div>
+        <WidgetContextMenu
+          v-if="isEditMode"
+          :widget="widget"
+          :dashboard="dashboard"
+          @delete-widget="$emit('delete-widget', $event)"
+        ></WidgetContextMenu>
       </div>
-      <WidgetContextMenu
-        v-if="isEditMode"
-        :widget="widget"
-        :dashboard="dashboard"
-        @delete-widget="$emit('delete-widget', $event)"
-      ></WidgetContextMenu>
-    </div>
-    <Chart :data-source="dataSource" :data-source-data="dataForDataSource">
-    </Chart>
+
+      <div class="dashboard-chart-widget__content widget__content">
+        <Chart :data-source="dataSource" :data-source-data="dataForDataSource">
+        </Chart>
+      </div>
+    </template>
+    <div v-else class="dashboard-chart-widget__loading loading-spinner"></div>
   </div>
 </template>
 
@@ -57,6 +67,16 @@ export default {
       required: false,
       default: '',
     },
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    editMode: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
   computed: {
     dataSource() {
diff --git a/web-frontend/modules/core/assets/scss/components/dashboard/all.scss b/web-frontend/modules/core/assets/scss/components/dashboard/all.scss
index c19a721f5..773991c1a 100644
--- a/web-frontend/modules/core/assets/scss/components/dashboard/all.scss
+++ b/web-frontend/modules/core/assets/scss/components/dashboard/all.scss
@@ -7,6 +7,6 @@
 @import 'dashboard_widget';
 @import 'dashboard_summary_widget';
 @import 'create_widget_button';
-@import 'widget_header';
+@import 'widget';
 @import 'widget_settings_base_form';
 @import 'create_widget_modal';
diff --git a/web-frontend/modules/core/assets/scss/components/dashboard/dashboard_summary_widget.scss b/web-frontend/modules/core/assets/scss/components/dashboard/dashboard_summary_widget.scss
index 20283267c..591f1c3f1 100644
--- a/web-frontend/modules/core/assets/scss/components/dashboard/dashboard_summary_widget.scss
+++ b/web-frontend/modules/core/assets/scss/components/dashboard/dashboard_summary_widget.scss
@@ -1,5 +1,5 @@
 .dashboard-summary-widget {
-  padding: 0 24px 24px;
+  // nothing
 }
 
 .dashboard-summary-widget__summary {
@@ -7,7 +7,6 @@
   font-size: 40px;
   font-weight: 600;
   line-height: 40px;
-  margin-top: 16px;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
@@ -16,3 +15,11 @@
 .dashboard-summary-widget__summary--misconfigured {
   color: #cdcecf;
 }
+
+.dashboard-summary-widget__loading {
+  height: 120px;
+
+  .dashboard-summary-widget--with-header-description & {
+    height: 141.8px;
+  }
+}
diff --git a/web-frontend/modules/core/assets/scss/components/dashboard/widget.scss b/web-frontend/modules/core/assets/scss/components/dashboard/widget.scss
new file mode 100644
index 000000000..91730655c
--- /dev/null
+++ b/web-frontend/modules/core/assets/scss/components/dashboard/widget.scss
@@ -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;
+}
diff --git a/web-frontend/modules/core/assets/scss/components/dashboard/widget_header.scss b/web-frontend/modules/core/assets/scss/components/dashboard/widget_header.scss
deleted file mode 100644
index f8b2d55ae..000000000
--- a/web-frontend/modules/core/assets/scss/components/dashboard/widget_header.scss
+++ /dev/null
@@ -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;
-}
diff --git a/web-frontend/modules/dashboard/components/widget/DashboardWidget.vue b/web-frontend/modules/dashboard/components/widget/DashboardWidget.vue
index f0bd7d66b..ab305bab3 100644
--- a/web-frontend/modules/dashboard/components/widget/DashboardWidget.vue
+++ b/web-frontend/modules/dashboard/components/widget/DashboardWidget.vue
@@ -12,14 +12,12 @@
     </div>
     <component
       :is="widgetComponent(widget.type)"
-      v-if="isLoading === false"
       :dashboard="dashboard"
       :widget="widget"
       :store-prefix="storePrefix"
+      :loading="isLoading"
+      :edit-mode="isEditMode"
     />
-    <div v-else>
-      <div class="dashboard-widget__loading"></div>
-    </div>
   </div>
 </template>
 
diff --git a/web-frontend/modules/dashboard/components/widget/SummaryWidget.vue b/web-frontend/modules/dashboard/components/widget/SummaryWidget.vue
index 3ff83e494..9d17efbc9 100644
--- a/web-frontend/modules/dashboard/components/widget/SummaryWidget.vue
+++ b/web-frontend/modules/dashboard/components/widget/SummaryWidget.vue
@@ -1,45 +1,46 @@
 <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"
+  <div
+    class="dashboard-summary-widget"
+    :class="{
+      'dashboard-summary-widget--with-header-description': widget.description,
+    }"
+  >
+    <template v-if="!loading">
+      <div class="widget__header widget__header--no-border">
+        <div class="widget__header-main">
+          <div class="widget__header-title-wrapper">
+            <div class="widget__header-title">{{ widget.title }}</div>
+
+            <Badge
+              v-if="dataSourceMisconfigured"
+              color="red"
+              indicator
+              rounded
+              >{{ $t('widget.fixConfiguration') }}</Badge
             >
-              <circle cx="2.5" cy="3" r="2.5" fill="#FF5A44" />
-            </svg>
-            {{ $t('widget.fixConfiguration') }}
+          </div>
+          <div v-if="widget.description" class="widget__header-description">
+            {{ widget.description }}
           </div>
         </div>
-        <div v-if="widget.description" class="widget-header__description">
-          {{ widget.description }}
-        </div>
+        <WidgetContextMenu
+          v-if="isEditMode"
+          :widget="widget"
+          :dashboard="dashboard"
+          @delete-widget="$emit('delete-widget', $event)"
+        ></WidgetContextMenu>
       </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
+        class="widget__content dashboard-summary-widget__summary"
+        :class="{
+          'dashboard-summary-widget__summary--misconfigured':
+            dataSourceMisconfigured,
+        }"
+      >
+        {{ result }}
+      </div>
+    </template>
+    <div v-else class="dashboard-summary-widget__loading loading-spinner"></div>
   </div>
 </template>
 
@@ -63,6 +64,11 @@ export default {
       required: false,
       default: '',
     },
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
   },
   computed: {
     dataSource() {
diff --git a/web-frontend/modules/dashboard/components/widget/WidgetContextMenu.vue b/web-frontend/modules/dashboard/components/widget/WidgetContextMenu.vue
index a465ed5e3..38b191d1c 100644
--- a/web-frontend/modules/dashboard/components/widget/WidgetContextMenu.vue
+++ b/web-frontend/modules/dashboard/components/widget/WidgetContextMenu.vue
@@ -1,11 +1,11 @@
 <template>
-  <div ref="contextButton" class="widget-header__context-menu">
+  <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)
+        $refs.context.toggle($refs.contextButton, 'bottom', 'right', 8, -8)
       "
     ></ButtonIcon>
     <WidgetContext