Create a new customization component based on the dashboard layout component
Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.
Problem to solve
As part our modular dashboard foundations we ant to deliver a new dashboard customization component that's based on the dashboard layout component.
Proposal
- Publish a new
customizable_dashboards.vuethat usesGlDashboardLayout.- See implementation notes for example.
Implementation notes
Guidelines
- Use the dashboard layout component as a base.
- Vue shared dashboard layout component
- OR from GitLab UI Add dashboard grid to GitLab UI (#542162 - closed) when ready
- Adhere to the blueprint Add dashboard customization framework design do... (gitlab-com/content-sites/handbook!14248 - merged)
- Customization component should be a drop in replacement for
dashboard_layout.vueusers.
- Customization component should be a drop in replacement for
Why a new component?
-
Passing the
editingstate to Gridstack. - Editing form + UI changes don't fit into the layout component.
- Additional UI controls for the editing mode don't have a place in the layout component.
Example code
Roughly working example that can be used to get started with.
Click to expand
<script>
import { GlButton, GlFormInput, GlFormGroup, GlIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
import { s__, __ } from '~/locale';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import DashboardLayout from './dashboard_layout.vue';
export default {
name: 'CustomizableDashboard',
components: {
DashboardLayout,
GlButton,
GlFormInput,
GlIcon,
GlFormGroup,
},
props: {
config: {
type: Object,
required: true,
},
isNewDashboard: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
editing: false,
titleValidationError: null,
mutableConfig: JSON.parse(JSON.stringify(this.config)),
};
},
computed: {
changesMade() {
// Compare the dashboard configs as that is what will be saved
return !isEqual(
this.config,
this.mutableConfig,
);
},
},
methods: {
async confirmDiscardChanges() {
const confirmText = this.isNewDashboard
? s__('Dashboards|Are you sure you want to cancel creating this dashboard?')
: s__('Dashboards|Are you sure you want to cancel editing this dashboard?');
const cancelBtnText = this.isNewDashboard
? s__('Dashboards|Continue creating')
: s__('Dashboards|Continue editing');
return confirmAction(confirmText, {
primaryBtnText: __('Discard changes'),
cancelBtnText,
});
},
onSave() {
this.$emit('save', this.mutableConfig);
},
async onCancel() {
if (this.changesMade) {
const confirmed = await this.confirmDiscardChanges();
if (!confirmed) return;
this.mutableConfig = JSON.parse(JSON.stringify(this.config));
}
this.editing = false;
},
onDashboardChanged(newConfig) {
this.mutableConfig = newConfig;
},
},
inheritAttrs: false,
};
</script>
<template>
<dashboard-layout
:config="mutableConfig"
:editing="editing"
v-bind="$attrs"
v-on="$listeners"
@input="onDashboardChanged"
>
<!-- Pass named slots -->
<template v-for="(_, name) in $slots" :slot="name">
<slot :name="name"></slot>
</template>
<!-- Pass scoped slots -->
<!-- Might have to filter out header -->
<template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="scope">
<slot :name="name" v-bind="scope"></slot>
</template>
<template #actions>
<gl-button
v-if="!editing"
icon="pencil"
class="gl-mr-2"
data-testid="dashboard-edit-btn"
@click="editing = true"
>{{ s__('Dashboards|Edit') }}</gl-button
>
<slot name="actions"></slot>
</template>
<template v-if="editing" #header>
<div class="gl-flex gl-w-full gl-flex-col">
<h2 class="gl-mb-6 gl-mt-0">
{{ s__('Dashboards|Edit your dashboard') }}
</h2>
<div class="flex-fill gl-flex gl-flex-col">
<gl-form-group
:label="s__('Dashboards|Dashboard title')"
label-for="title"
:class="$options.FORM_GROUP_CLASS"
class="gl-mb-4"
data-testid="dashboard-title-form-group"
:invalid-feedback="titleValidationError"
:state="!titleValidationError"
>
<gl-form-input
id="title"
ref="titleInput"
v-model="mutableConfig.title"
dir="auto"
type="text"
:placeholder="s__('Dashboards|Enter a dashboard title')"
:aria-label="s__('Dashboards|Dashboard title')"
:class="$options.FORM_INPUT_CLASS"
data-testid="dashboard-title-input"
:state="!titleValidationError"
required
/>
</gl-form-group>
<gl-form-group
:label="s__('Dashboards|Dashboard description (optional)')"
label-for="description"
:class="$options.FORM_GROUP_CLASS"
>
<gl-form-input
id="description"
v-model="mutableConfig.description"
dir="auto"
type="text"
:placeholder="s__('Dashboards|Enter a dashboard description')"
:aria-label="s__('Dashboards|Dashboard description')"
:class="$options.FORM_INPUT_CLASS"
data-testid="dashboard-description-input"
/>
</gl-form-group>
</div>
</div>
</template>
<template v-if="editing" #footer>
<gl-button
class="gl-my-4 gl-mr-2"
category="primary"
variant="confirm"
data-testid="dashboard-save-btn"
@click="onSave"
>
{{ s__('Dashboards|Save your dashboard') }}
</gl-button>
<gl-button category="secondary" data-testid="dashboard-cancel-edit-btn" @click="onCancel">{{
s__('Dashboards|Cancel')
}}</gl-button>
</template>
</dashboard-layout>
</template>
Edited by 🤖 GitLab Bot 🤖