fix: responsive dashboard grid (#12608)

This commit is contained in:
Barthélémy Ledoux
2025-11-05 16:44:41 +01:00
committed by GitHub
parent 7ba29e593f
commit f7e3d1e6c5
8 changed files with 152 additions and 67 deletions

View File

@@ -28,33 +28,10 @@
}
}
async function loadChart(chart: any) {
const yamlChart = YAML_UTILS.stringify(chart);
const result: { error: string | null; data: null | {
id?: string;
name?: string;
type?: string;
chartOptions?: Record<string, any>;
dataFilters?: any[];
charts?: any[];
}; raw: any } = {
error: null,
data: null,
raw: {}
};
const errors = await dashboardStore.validateChart(yamlChart);
if (errors.constraints) {
result.error = errors.constraints;
} else {
result.data = {...chart, content: yamlChart, raw: chart};
}
return result;
}
async function updateChartPreview(event: any) {
const chart = YAML_UTILS.getChartAtPosition(event.model.getValue(), event.position);
if (chart) {
const result = await loadChart(chart);
const result = await dashboardStore.loadChart(chart);
dashboardStore.selectedChart = typeof result.data === "object"
? {
...result.data,

View File

@@ -37,6 +37,7 @@
FIELDNAME_INJECTION_KEY,
FULL_SCHEMA_INJECTION_KEY,
FULL_SOURCE_INJECTION_KEY,
ON_TASK_EDITOR_CLICK_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY,
POSITION_INJECTION_KEY,
REF_PATH_INJECTION_KEY,
@@ -111,6 +112,15 @@
provide(BLOCK_SCHEMA_PATH_INJECTION_KEY, computed(() => props.blockSchemaPath ?? dashboardStore.schema.$ref ?? ""));
provide(FULL_SOURCE_INJECTION_KEY, computed(() => dashboardStore.sourceCode ?? ""));
provide(POSITION_INJECTION_KEY, props.position ?? "after");
provide(ON_TASK_EDITOR_CLICK_INJECTION_KEY, (elt) => {
const type = elt?.type;
dashboardStore.loadChart(elt);
if(type){
pluginsStore.updateDocumentation({type});
}else{
pluginsStore.updateDocumentation();
}
})
const pluginsStore = usePluginsStore();

View File

@@ -1,6 +1,7 @@
<template>
<div class="w-100 p-4">
<Sections
:key="dashboardStore.sourceCode"
:dashboard="{id: 'default', charts: []}"
:charts="charts.map(chart => chart.data).filter(chart => chart !== null)"
showDefault
@@ -9,11 +10,12 @@
</template>
<script lang="ts" setup>
import {onMounted, ref} from "vue";
import {ref, watch} from "vue";
import Sections from "../sections/Sections.vue";
import {Chart} from "../composables/useDashboards";
import {useDashboardStore} from "../../../stores/dashboard";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import throttle from "lodash/throttle";
interface Result {
error: string[] | null;
@@ -23,21 +25,27 @@
const charts = ref<Result[]>([])
onMounted(async () => {
validateAndLoadAllCharts();
});
const dashboardStore = useDashboardStore();
function validateAndLoadAllCharts() {
charts.value = [];
const validateAndLoadAllChartsThrottled = throttle(validateAndLoadAllCharts, 500);
async function validateAndLoadAllCharts() {
const allCharts = YAML_UTILS.getAllCharts(dashboardStore.sourceCode) ?? [];
allCharts.forEach(async (chart: any) => {
const loadedChart = await loadChart(chart);
charts.value.push(loadedChart);
});
charts.value = await Promise.all(allCharts.map(async (chart: any) => {
return loadChart(chart);
}));
}
watch(
() => dashboardStore.sourceCode,
() => {
validateAndLoadAllChartsThrottled();
}
, {immediate: true}
);
async function loadChart(chart: any) {
const yamlChart = YAML_UTILS.stringify(chart);
const result: Result = {

View File

@@ -1,12 +1,13 @@
<template>
<section id="charts" :class="{padding}">
<el-row :gutter="16">
<el-col
<div class="dashboard-sections-container">
<section id="charts" :class="{padding}">
<div
v-for="chart in props.charts"
:key="`chart__${chart.id}`"
:xs="24"
:sm="(chart.chartOptions?.width || 6) * 4"
:md="(chart.chartOptions?.width || 6) * 2"
class="dashboard-block"
:class="{
[`dash-width-${chart.chartOptions?.width || 6}`]: true
}"
>
<div class="d-flex flex-column">
<div class="d-flex justify-content-between">
@@ -64,9 +65,9 @@
/>
</div>
</div>
</el-col>
</el-row>
</section>
</div>
</section>
</div>
</template>
<script setup lang="ts">
@@ -133,14 +134,28 @@
<style scoped lang="scss">
@import "@kestra-io/ui-libs/src/scss/variables";
.dashboard-sections-container{
container-type: inline-size;
}
$smallMobile: 375px;
$tablet: 768px;
section#charts {
display: grid;
gap: 1rem;
grid-template-columns: repeat(3, 1fr);
@container (min-width: #{$smallMobile}) {
grid-template-columns: repeat(6, 1fr);
}
@container (min-width: #{$tablet}) {
grid-template-columns: repeat(12, 1fr);
}
&.padding {
padding: 0 2rem 1rem;
}
& .el-row .el-col {
margin-bottom: 1rem;
.dashboard-block {
& > div {
height: 100%;
padding: 1.5rem;
@@ -159,5 +174,24 @@ section#charts {
opacity: 1;
}
}
.dash-width-3, .dash-width-6, .dash-width-9, .dash-width-12 {
grid-column: span 3;
}
@container (min-width: #{$smallMobile}) {
.dash-width-6, .dash-width-9, .dash-width-12 {
grid-column: span 6;
}
}
@container (min-width: #{$tablet}) {
.dash-width-9 {
grid-column: span 9;
}
.dash-width-12 {
grid-column: span 12;
}
}
}
</style>

View File

@@ -39,7 +39,7 @@ export const decodeSearchParams = (query: LocationQuery) =>
operation
};
})
.filter(Boolean);
.filter(v => v !== null);
type Filter = Pick<AppliedFilter, "key" | "comparator" | "value">;

View File

@@ -1,6 +1,6 @@
<template>
<div v-if="playgroundStore.enabled && isTask && taskObject?.id" class="flow-playground">
<PlaygroundRunTaskButton :taskId="taskObject?.id" />
<div v-if="playgroundStore.enabled && isTask && taskModel?.id" class="flow-playground">
<PlaygroundRunTaskButton :taskId="taskModel?.id" />
</div>
<el-form v-if="isTaskDefinitionBasedOnType" labelPosition="top">
<el-form-item>
@@ -17,12 +17,12 @@
/>
</el-form-item>
</el-form>
<div @click="isPlugin && pluginsStore.updateDocumentation(taskObject as Parameters<typeof pluginsStore.updateDocumentation>[0])">
<div @click="() => onTaskEditorClick(taskModel)">
<TaskObject
v-loading="isLoading"
v-if="(selectedTaskType || !isTaskDefinitionBasedOnType) && schema"
name="root"
:modelValue="taskObject"
:modelValue="taskModel"
@update:model-value="onTaskInput"
:schema
:properties
@@ -43,6 +43,7 @@
FULL_SCHEMA_INJECTION_KEY,
SCHEMA_DEFINITIONS_INJECTION_KEY,
DATA_TYPES_MAP_INJECTION_KEY,
ON_TASK_EDITOR_CLICK_INJECTION_KEY,
} from "../injectionKeys";
import {removeNullAndUndefined} from "../utils/cleanUp";
import {removeRefPrefix, usePluginsStore} from "../../../stores/plugins";
@@ -63,9 +64,9 @@
const pluginsStore = usePluginsStore();
const playgroundStore = usePlaygroundStore();
type PartialCodeElement = Partial<NoCodeElement>;
type PartialNoCodeElement = Partial<NoCodeElement>;
const taskObject = ref<PartialCodeElement | undefined>({});
const taskModel = ref<PartialNoCodeElement | undefined>({});
const selectedTaskType = ref<string>();
const isLoading = ref(false);
@@ -108,7 +109,7 @@
watch(modelValue, (v) => {
if (!v) {
taskObject.value = {};
taskModel.value = {};
selectedTaskType.value = undefined;
} else {
setup()
@@ -150,20 +151,20 @@
});
function setup() {
const parsed = YAML_UTILS.parse<PartialCodeElement>(modelValue.value);
const parsed = YAML_UTILS.parse<PartialNoCodeElement>(modelValue.value);
if(isPluginDefaults.value){
const {forced, type, values} = parsed as any;
taskObject.value = {...values, forced, type};
taskModel.value = {...values, forced, type};
}else{
taskObject.value = parsed;
taskModel.value = parsed;
}
selectedTaskType.value = taskObject.value?.type;
selectedTaskType.value = taskModel.value?.type;
}
// when tab is opened, load the documentation
onActivated(() => {
if(selectedTaskType.value && parentPath !== "inputs"){
pluginsStore.updateDocumentation(taskObject.value as Parameters<typeof pluginsStore.updateDocumentation>[0]);
pluginsStore.updateDocumentation(taskModel.value as Parameters<typeof pluginsStore.updateDocumentation>[0]);
}
});
@@ -218,7 +219,7 @@
const resolvedType = computed<string>(() => {
if(resolvedTypes.value.length > 1 && selectedTaskType.value){
// find the resolvedType that match the current dataType
const dataType = taskObject.value?.data?.type;
const dataType = taskModel.value?.data?.type;
if(dataType){
for(const typeLocal of resolvedTypes.value){
const schema = definitions.value?.[typeLocal];
@@ -330,13 +331,13 @@
watch([selectedTaskType, fullSchema], ([task]) => {
if (task) {
if(isPlugin.value){
pluginsStore.updateDocumentation(taskObject.value as Parameters<typeof pluginsStore.updateDocumentation>[0]);
pluginsStore.updateDocumentation(taskModel.value as Parameters<typeof pluginsStore.updateDocumentation>[0]);
}
}
}, {immediate: true});
function onTaskInput(val: PartialCodeElement | undefined) {
taskObject.value = val;
function onTaskInput(val: PartialNoCodeElement | undefined) {
taskModel.value = val;
if(fieldName){
val = {
[fieldName]: val,
@@ -362,12 +363,21 @@
}
function onTaskTypeSelect() {
const value: PartialCodeElement = {
const value: PartialNoCodeElement = {
type: selectedTaskType.value ?? ""
};
onTaskInput(value);
}
const onTaskEditorClick = inject(ON_TASK_EDITOR_CLICK_INJECTION_KEY, (elt?: PartialNoCodeElement) => {
const type = elt?.type;
if(isPlugin.value && type){
pluginsStore.updateDocumentation({type});
}else{
pluginsStore.updateDocumentation();
}
});
</script>
<style scoped lang="scss">

View File

@@ -1,5 +1,5 @@
import type {ComputedRef, InjectionKey, Ref} from "vue"
import {TopologyClickParams} from "./utils/types"
import {NoCodeElement, TopologyClickParams} from "./utils/types"
import {Panel} from "../../utils/multiPanelTypes"
export const BLOCK_SCHEMA_PATH_INJECTION_KEY = Symbol("block-schema-path-injection-key") as InjectionKey<ComputedRef<string>>
@@ -90,4 +90,6 @@ export const FULL_SCHEMA_INJECTION_KEY = Symbol("full-schema-injection-key") as
export const SCHEMA_DEFINITIONS_INJECTION_KEY = Symbol("schema-definitions-injection-key") as InjectionKey<ComputedRef<Record<string, any>>>
export const DATA_TYPES_MAP_INJECTION_KEY = Symbol("data-types-injection-key") as InjectionKey<ComputedRef<Record<string, string[] | undefined>>>
export const DATA_TYPES_MAP_INJECTION_KEY = Symbol("data-types-injection-key") as InjectionKey<ComputedRef<Record<string, string[] | undefined>>>
export const ON_TASK_EDITOR_CLICK_INJECTION_KEY = Symbol("on-task-editor-click-injection-key") as InjectionKey<(elt?: Partial<NoCodeElement>) => void>;

View File

@@ -140,6 +140,49 @@ export const useDashboardStore = defineStore("dashboard", () => {
return rootSchema.value?.properties;
});
async function loadChart(chart: any) {
const yamlChart = YAML_UTILS.stringify(chart);
if(selectedChart.value?.content === yamlChart){
return {
error: chartErrors.value.length > 0 ? chartErrors.value[0] : null,
data: selectedChart.value ? {...selectedChart.value, raw: chart} : null,
raw: chart
};
}
const result: { error: string | null; data: null | {
id?: string;
name?: string;
type?: string;
chartOptions?: Record<string, any>;
dataFilters?: any[];
charts?: any[];
}; raw: any } = {
error: null,
data: null,
raw: {}
};
const errors = await validateChart(yamlChart);
if (errors.constraints) {
result.error = errors.constraints;
} else {
result.data = {...chart, content: yamlChart, raw: chart};
}
selectedChart.value = typeof result.data === "object"
? {
...result.data,
chartOptions: {
...result.data?.chartOptions,
width: 12
}
} as any
: undefined;
chartErrors.value = [result.error].filter(e => e !== null);
return result;
}
return {
dashboard,
chartErrors,
@@ -155,6 +198,7 @@ export const useDashboardStore = defineStore("dashboard", () => {
validateChart,
chartPreview,
export: exportDashboard,
loadChart,
schema,
definitions,