mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
fix: dashboard conflict issue (#7558)
### **PR Type** Bug fix, Enhancement ___ ### **Description** - Validate dashboard JSON before import - Handle duplicate panel IDs in retrieval - Refine GridStack layout update logic - Improve error and import messages ___ ### **Changes diagram** ```mermaid flowchart LR node1["getDashboard called"] node2["fixDuplicatePanelIds"] node3["duplicates?"] node4["updateDashboard & re-fetch"] node5["return dashboard"] node1 --> node2 node2 --> node3 node3 -- "true" --> node4 node3 -- "false" --> node5 node4 --> node5 ``` ___ ### **Changes walkthrough** 📝 <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Error handling</strong></td><td><table> <tr> <td> <details> <summary><strong>ImportDashboard.vue</strong><dd><code>Add dashboard JSON validation before import</code> </dd></summary> <hr> web/src/views/Dashboards/ImportDashboard.vue <li>Imported <code>validateDashboardJson</code> utility<br> <li> Added schema validation before each import step<br> <li> Extended error <code><span></code> for <code>dashboard_validation</code><br> <li> Reformatted promise and notification code </details> </td> <td><a href="https://github.com/openobserve/openobserve/pull/7558/files#diff-660ac197e10735b4f08fc4b5a39744187d16c14156ced00eefde09dbaec323f0">+85/-25</a> </td> </tr> </table></td></tr><tr><td><strong>Enhancement</strong></td><td><table> <tr> <td> <details> <summary><strong>RenderDashboardCharts.vue</strong><dd><code>Improve GridStack layout update logic</code> </dd></summary> <hr> web/src/views/Dashboards/RenderDashboardCharts.vue <li>Included <code>selectedTabId</code> in panel <code>:key</code><br> <li> Added <code>gridStackUpdateInProgress</code> guard<br> <li> Optimized <code>refreshGridStack</code> clearing logic<br> <li> Added console error logs on save and widget add </details> </td> <td><a href="https://github.com/openobserve/openobserve/pull/7558/files#diff-313f7c69102161242c893c863c50f1f1cd1fc19b68b5c99bf840b4b5c72f0f2e">+51/-42</a> </td> </tr> </table></td></tr><tr><td><strong>Bug fix</strong></td><td><table> <tr> <td> <details> <summary><strong>commons.ts</strong><dd><code>Handle duplicate panel IDs on retrieval</code> </dd></summary> <hr> web/src/utils/commons.ts <li>Added <code>fixDuplicatePanelIds</code> to dedupe panel IDs<br> <li> Called dedupe in <code>getDashboard</code> flow<br> <li> Saved and re-fetched dashboard when duplicates found<br> <li> Implemented <code>generateUniquePanelId</code> helper </details> </td> <td><a href="https://github.com/openobserve/openobserve/pull/7558/files#diff-17cb74f8142984fc46bbfecdeaacccc118fd5d4ab7f040a55695caaa309d104f">+81/-14</a> </td> </tr> </table></td></tr></tr></tbody></table> ___ > <details> <summary> Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about PR-Agent usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details> --------- Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
This commit is contained in:
co-authored by
Kirtan Thakkar
parent
b057aeb4d0
commit
935dfbad61
+81
-14
@@ -557,26 +557,93 @@ export const getDashboard = async (
|
||||
folderId: any,
|
||||
) => {
|
||||
// check if dashboard data is present in store
|
||||
if (store.state.organizationData.allDashboardData[dashboardId]) {
|
||||
return store.state.organizationData.allDashboardData[dashboardId];
|
||||
let dashboardJson =
|
||||
store.state.organizationData.allDashboardData[dashboardId];
|
||||
|
||||
if (!dashboardJson && dashboardId) {
|
||||
const apiResponse = await dashboardService.get_Dashboard(
|
||||
store.state.selectedOrganization.identifier,
|
||||
dashboardId,
|
||||
folderId,
|
||||
);
|
||||
|
||||
dashboardJson = await retrieveAndStoreDashboardData(
|
||||
store,
|
||||
dashboardId,
|
||||
folderId,
|
||||
apiResponse,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dashboardId) {
|
||||
if (!dashboardJson) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const apiResponse = await dashboardService.get_Dashboard(
|
||||
store.state.selectedOrganization.identifier,
|
||||
dashboardId,
|
||||
folderId,
|
||||
);
|
||||
// Fix duplicate panel IDs and check if any were found
|
||||
const hasDuplicates = fixDuplicatePanelIds(dashboardJson);
|
||||
|
||||
return await retrieveAndStoreDashboardData(
|
||||
store,
|
||||
dashboardId,
|
||||
folderId,
|
||||
apiResponse,
|
||||
);
|
||||
// If duplicates were found, save the dashboard and retrieve it with new IDs
|
||||
if (hasDuplicates) {
|
||||
// Save the dashboard with fixed IDs
|
||||
await updateDashboard(
|
||||
store,
|
||||
store.state.selectedOrganization.identifier,
|
||||
dashboardId,
|
||||
dashboardJson,
|
||||
folderId ?? "default",
|
||||
);
|
||||
|
||||
// Retrieve the dashboard again to get the updated version with new IDs
|
||||
const apiResponse = await dashboardService.get_Dashboard(
|
||||
store.state.selectedOrganization.identifier,
|
||||
dashboardId,
|
||||
folderId,
|
||||
);
|
||||
|
||||
dashboardJson = await retrieveAndStoreDashboardData(
|
||||
store,
|
||||
dashboardId,
|
||||
folderId,
|
||||
apiResponse,
|
||||
);
|
||||
}
|
||||
|
||||
return dashboardJson;
|
||||
};
|
||||
|
||||
// Fix duplicate panel.id values by assigning unique ones
|
||||
const PANEL_ID_PREFIX = "Panel_ID";
|
||||
|
||||
const fixDuplicatePanelIds = (dashboardJson: any): boolean => {
|
||||
const panelIdSet = new Set<string>();
|
||||
let hasDuplicates = false;
|
||||
|
||||
for (const tab of dashboardJson?.tabs || []) {
|
||||
if (!Array.isArray(tab?.panels)) continue;
|
||||
|
||||
for (const panel of tab.panels) {
|
||||
const originalId = panel?.id;
|
||||
if (!originalId) continue;
|
||||
|
||||
if (panelIdSet.has(originalId)) {
|
||||
const newId = generateUniquePanelId(panelIdSet);
|
||||
panel.id = newId;
|
||||
hasDuplicates = true;
|
||||
}
|
||||
|
||||
panelIdSet.add(panel.id);
|
||||
}
|
||||
}
|
||||
|
||||
return hasDuplicates;
|
||||
};
|
||||
|
||||
const generateUniquePanelId = (existingIds: Set<string>): string => {
|
||||
let id;
|
||||
do {
|
||||
id = `${PANEL_ID_PREFIX}${Math.floor(1000000 + Math.random() * 9000000)}`;
|
||||
} while (existingIds.has(id));
|
||||
return id;
|
||||
};
|
||||
|
||||
export const deleteDashboardById = async (
|
||||
|
||||
@@ -303,7 +303,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span v-else>{{ errorMessage }}</span>
|
||||
<span
|
||||
v-else-if="errorMessage.field == 'dashboard_validation'"
|
||||
class="text-red"
|
||||
>
|
||||
{{ errorMessage.message }}
|
||||
</span>
|
||||
|
||||
<span v-else>{{
|
||||
errorMessage.message || errorMessage
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,6 +335,7 @@ import { useRouter, useRoute } from "vue-router";
|
||||
import dashboardService from "../../services/dashboards.js";
|
||||
import axios from "axios";
|
||||
import { convertDashboardSchemaVersion } from "@/utils/dashboard/convertDashboardSchemaVersion";
|
||||
import { validateDashboardJson } from "@/utils/dashboard/convertDataIntoUnitValue";
|
||||
import SelectFolderDropdown from "@/components/dashboards/SelectFolderDropdown.vue";
|
||||
import useNotifications from "@/composables/useNotifications";
|
||||
import AppTabs from "@/components/common/AppTabs.vue";
|
||||
@@ -529,10 +539,10 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const data = jsonStr.value.map((parsedContent, fileIndex) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const fileName = jsonFiles.value[fileIndex]?.name || `File ${fileIndex + 1}`;
|
||||
const fileName =
|
||||
jsonFiles.value[fileIndex]?.name || `File ${fileIndex + 1}`;
|
||||
|
||||
try {
|
||||
//this is done because if the user uploads a single dashboard, it will be an object and if the user uploads multiple dashboards, it will be an array of objects
|
||||
@@ -550,8 +560,24 @@ export default defineComponent({
|
||||
//it will convert the dashboard schema version to the latest version
|
||||
|
||||
try {
|
||||
const convertedSchema = convertDashboardSchemaVersion(dashboard);
|
||||
const res = await importDashboardFromJSON(convertedSchema, selectedFolder.value);
|
||||
const convertedSchema =
|
||||
convertDashboardSchemaVersion(dashboard);
|
||||
|
||||
// Validate the converted schema before importing
|
||||
const validationErrors = validateDashboardJson(convertedSchema);
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessage = validationErrors.join("; ");
|
||||
results.push({
|
||||
index: i + 1,
|
||||
error: new Error(errorMessage),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const res = await importDashboardFromJSON(
|
||||
convertedSchema,
|
||||
selectedFolder.value,
|
||||
);
|
||||
results.push({ index: i + 1, result: res });
|
||||
} catch (e) {
|
||||
results.push({ index: i + 1, error: e });
|
||||
@@ -559,17 +585,17 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
const failedMessages = results
|
||||
.filter(r => r.error)
|
||||
.map(r => `${r.error?.message || r.error}`);
|
||||
.filter((r) => r.error)
|
||||
.map((r) => `${r.error?.message || r.error}`);
|
||||
|
||||
if (failedMessages.length) {
|
||||
reject({
|
||||
file: `JSON ${fileIndex + 1}`,
|
||||
error: failedMessages.join("; "),
|
||||
});
|
||||
} else {
|
||||
resolve({ file: fileName, results });
|
||||
}
|
||||
if (failedMessages.length) {
|
||||
reject({
|
||||
file: `JSON ${fileIndex + 1}`,
|
||||
error: failedMessages.join("; "),
|
||||
});
|
||||
} else {
|
||||
resolve({ file: fileName, results });
|
||||
}
|
||||
} catch (e) {
|
||||
reject({ file: fileName, error: "Error processing file" });
|
||||
}
|
||||
@@ -579,14 +605,18 @@ export default defineComponent({
|
||||
Promise.allSettled(data).then(async (results) => {
|
||||
filesImportResults.value = results;
|
||||
|
||||
const successfulImports = results.filter(r => r.status === "fulfilled").length;
|
||||
const successfulImports = results.filter(
|
||||
(r) => r.status === "fulfilled",
|
||||
).length;
|
||||
|
||||
if (results.length === successfulImports) {
|
||||
await resetAndRefresh(ImportType.FILES, selectedFolder.value);
|
||||
}
|
||||
|
||||
if (successfulImports) {
|
||||
showPositiveNotification(`${successfulImports} File(s) Imported Successfully`);
|
||||
showPositiveNotification(
|
||||
`${successfulImports} File(s) Imported Successfully`,
|
||||
);
|
||||
}
|
||||
|
||||
const failedImports = results.length - successfulImports;
|
||||
@@ -598,7 +628,6 @@ export default defineComponent({
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// reset and refresh the value based on selected type
|
||||
const resetAndRefresh = async (type, selectedFolder) => {
|
||||
switch (type) {
|
||||
@@ -644,11 +673,18 @@ export default defineComponent({
|
||||
//Example: if user uploads a single object file it will be converted to an array and if user uploads a array of objects it is already an array so we dont do anything
|
||||
const rawJson = JSON.parse(jsonStr.value);
|
||||
const dashboards = Array.isArray(rawJson) ? rawJson : [rawJson];
|
||||
|
||||
|
||||
const importPromises = dashboards.map((dashboard, index) => {
|
||||
try {
|
||||
const converted = convertDashboardSchemaVersion(dashboard);
|
||||
|
||||
// Validate the converted schema before importing
|
||||
const validationErrors = validateDashboardJson(converted);
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessage = validationErrors.join("; ");
|
||||
return Promise.reject({ index, error: new Error(errorMessage) });
|
||||
}
|
||||
|
||||
return importDashboardFromJSON(converted, selectedFolder.value);
|
||||
} catch (e) {
|
||||
return Promise.reject({ index, error: e });
|
||||
@@ -657,12 +693,16 @@ export default defineComponent({
|
||||
|
||||
const results = await Promise.allSettled(importPromises);
|
||||
|
||||
const successCount = results.filter(r => r.status === 'fulfilled').length;
|
||||
const successCount = results.filter(
|
||||
(r) => r.status === "fulfilled",
|
||||
).length;
|
||||
const failedCount = results.length - successCount;
|
||||
|
||||
if (successCount > 0) {
|
||||
await resetAndRefresh(ImportType.URL, selectedFolder.value);
|
||||
showPositiveNotification(`${successCount} Dashboard(s) Imported Successfully`);
|
||||
showPositiveNotification(
|
||||
`${successCount} Dashboard(s) Imported Successfully`,
|
||||
);
|
||||
}
|
||||
|
||||
if (failedCount > 0) {
|
||||
@@ -674,13 +714,12 @@ export default defineComponent({
|
||||
showErrorNotification("Failed to Import Dashboard");
|
||||
} finally {
|
||||
if (jsonStr.value && typeof jsonStr.value !== "string") {
|
||||
jsonStr.value = "";
|
||||
}
|
||||
jsonStr.value = "";
|
||||
}
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// import dashboard from json string
|
||||
const importFromJsonStr = async () => {
|
||||
isLoading.value = ImportType.JSON_STRING;
|
||||
@@ -691,6 +730,14 @@ export default defineComponent({
|
||||
const convertedSchema =
|
||||
convertDashboardSchemaVersion(oldImportedSchema);
|
||||
|
||||
// Validate the converted schema before importing
|
||||
const validationErrors = validateDashboardJson(convertedSchema);
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessage = validationErrors.join("; ");
|
||||
showErrorNotification(`Validation failed: ${errorMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await importDashboardFromJSON(
|
||||
convertedSchema,
|
||||
selectedFolder.value,
|
||||
@@ -759,6 +806,7 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
const validateBasicInputs = (input, index = 0) => {
|
||||
// Basic title validation
|
||||
if (input.title === "" || typeof input.title !== "string") {
|
||||
dashboardErrorsToDisplay.value.push({
|
||||
message: `Title is required for dashboard - ${index ? index + 1 : 1} and should be a string`,
|
||||
@@ -766,6 +814,18 @@ export default defineComponent({
|
||||
dashboardIndex: index,
|
||||
});
|
||||
}
|
||||
|
||||
// Comprehensive dashboard validation using validateDashboardJson
|
||||
const validationErrors = validateDashboardJson(input);
|
||||
if (validationErrors.length > 0) {
|
||||
validationErrors.forEach((error) => {
|
||||
dashboardErrorsToDisplay.value.push({
|
||||
message: `Dashboard ${index ? index + 1 : 1}: ${error}`,
|
||||
field: "dashboard_validation",
|
||||
dashboardIndex: index,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const goToCommunityDashboards = () => {
|
||||
|
||||
@@ -86,7 +86,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
>
|
||||
<div
|
||||
v-for="item in panels"
|
||||
:key="item.id"
|
||||
:key="item.id + selectedTabId"
|
||||
:gs-id="item.id"
|
||||
:gs-x="getPanelLayout(item, 'x')"
|
||||
:gs-y="getPanelLayout(item, 'y')"
|
||||
@@ -465,6 +465,7 @@ export default defineComponent({
|
||||
|
||||
// refresh dashboard
|
||||
refreshDashboard();
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
@@ -479,8 +480,12 @@ export default defineComponent({
|
||||
tab: route.query.tab ?? props.dashboardData.panels[0]?.tabId,
|
||||
},
|
||||
});
|
||||
}; // GridStack initialization and methods
|
||||
};
|
||||
// GridStack initialization and methods
|
||||
|
||||
let gridStackUpdateInProgress = false;
|
||||
const initGridStack = () => {
|
||||
|
||||
if (!gridStackContainer.value || gridStackInstance) return;
|
||||
|
||||
// Initialize GridStack with optimal configuration
|
||||
@@ -508,9 +513,14 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
// Event listeners for GridStack interactions
|
||||
|
||||
// Handle layout changes (drag/resize)
|
||||
gridStackInstance.on("change", (event, items) => {
|
||||
|
||||
// Handle layout changes (drag/resize) - only update layout data, don't save during operations
|
||||
gridStackInstance.on("change", async (event, items) => {
|
||||
|
||||
if(gridStackUpdateInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (items && items.length > 0) {
|
||||
updatePanelLayouts(items); // Update panel layout data
|
||||
saveDashboardData.execute(); // Save changes to backend
|
||||
@@ -521,7 +531,7 @@ export default defineComponent({
|
||||
gridStackInstance.on("resizestop", (event, element) => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
});
|
||||
}; // Update panel layout data from GridStack items
|
||||
}; // Update panel layout data from GridStack items
|
||||
const updatePanelLayouts = (items) => {
|
||||
items.forEach((item) => {
|
||||
const panelId = item.id;
|
||||
@@ -534,9 +544,13 @@ export default defineComponent({
|
||||
panel.layout.h = item.h;
|
||||
}
|
||||
});
|
||||
}; // Optimized GridStack refresh function
|
||||
}; // Optimized GridStack refresh function
|
||||
const refreshGridStack = async () => {
|
||||
if (!gridStackInstance || !gridStackContainer.value) return;
|
||||
if (!gridStackInstance || !gridStackContainer.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
gridStackUpdateInProgress = true;
|
||||
|
||||
// Wait for Vue to finish DOM updates
|
||||
await nextTick();
|
||||
@@ -546,35 +560,24 @@ export default defineComponent({
|
||||
|
||||
// IMPORTANT: Disable animation and floating during reconstruction for better performance
|
||||
grid.float(false);
|
||||
grid.setAnimation(false); // Clear all existing widgets completely to prevent stale references
|
||||
grid.setAnimation(false);
|
||||
|
||||
// Clear all existing widgets completely to prevent stale references
|
||||
const existingElements = grid.getGridItems();
|
||||
|
||||
existingElements.forEach((element) => {
|
||||
grid.removeWidget(element, false);
|
||||
});
|
||||
|
||||
|
||||
// Force clear any remaining grid state
|
||||
grid.removeAll(false);
|
||||
|
||||
// Wait for DOM cleanup to complete
|
||||
await nextTick(); // Ensure DOM is ready
|
||||
|
||||
|
||||
if (panels.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort panels by their Y position first, then X position for optimal layout
|
||||
const sortedPanels = [...panels.value].sort((a, b) => {
|
||||
const aY = getPanelLayout(a, "y");
|
||||
const bY = getPanelLayout(b, "y");
|
||||
if (aY !== bY) return aY - bY;
|
||||
return getPanelLayout(a, "x") - getPanelLayout(b, "x");
|
||||
});
|
||||
|
||||
// Add panels in sorted order to maintain proper layout
|
||||
for (const panel of sortedPanels) {
|
||||
for (const panel of panels.value) {
|
||||
// Wait for the element to be available in DOM
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const element = gridStackContainer.value.querySelector(
|
||||
`[gs-id="${panel.id}"]`,
|
||||
);
|
||||
@@ -590,7 +593,7 @@ export default defineComponent({
|
||||
minH: getMinimumHeight(panel.type),
|
||||
id: panel.id,
|
||||
noMove: props.viewOnly,
|
||||
noResize: props.viewOnly,
|
||||
noResize: props.viewOnly,
|
||||
};
|
||||
|
||||
// Make widget with explicit layout
|
||||
@@ -604,10 +607,12 @@ export default defineComponent({
|
||||
// Wait for all widgets to be added
|
||||
await nextTick(); // Ensure DOM is updated
|
||||
|
||||
|
||||
// Trigger window resize to ensure charts render correctly
|
||||
gridStackUpdateInProgress = false;
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// Add a method to reset grid layout
|
||||
const resetGridLayout = async () => {
|
||||
if (!gridStackInstance) return;
|
||||
@@ -635,8 +640,8 @@ export default defineComponent({
|
||||
return panelData?.layout?.i || panelData.id;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// Get minimum height based on panel type for optimal display
|
||||
const getMinimumHeight = (type) => {
|
||||
switch (type) {
|
||||
@@ -654,7 +659,7 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
// Get minimum width based on panel type for optimal display
|
||||
// Get minimum width based on panel type for optimal display
|
||||
const getMinimumWidth = (type) => {
|
||||
switch (type) {
|
||||
case "area":
|
||||
@@ -685,13 +690,14 @@ export default defineComponent({
|
||||
|
||||
watch(
|
||||
() => [selectedTabId.value],
|
||||
async (newPanels, oldPanels) => {
|
||||
async (newPanels, oldPanels) => {
|
||||
// Only refresh if the number of tab changes
|
||||
await nextTick();
|
||||
await refreshGridStack();
|
||||
},
|
||||
{ deep: true }, // Deep watch to catch layout changes within panels
|
||||
);
|
||||
|
||||
|
||||
// Initialize GridStack when component is mounted
|
||||
onMounted(async () => {
|
||||
await nextTick(); // Wait for DOM to be ready
|
||||
@@ -701,7 +707,6 @@ export default defineComponent({
|
||||
|
||||
// Clean up GridStack instance before component unmounts to prevent memory leaks
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
// Clean up GridStack instance
|
||||
if (gridStackInstance) {
|
||||
gridStackInstance.off("change");
|
||||
@@ -783,7 +788,7 @@ export default defineComponent({
|
||||
isDashboardVariablesAndPanelsDataLoadedDebouncedValue,
|
||||
currentQueryTraceIds,
|
||||
openEditLayout,
|
||||
saveDashboardData,
|
||||
saveDashboardData,
|
||||
currentVariablesDataRef,
|
||||
resetGridLayout,
|
||||
refreshGridStack,
|
||||
@@ -829,7 +834,7 @@ export default defineComponent({
|
||||
.grid-stack {
|
||||
background: transparent;
|
||||
margin: 2px;
|
||||
|
||||
|
||||
/* When grid is static (disabled), hide resize handles */
|
||||
&.grid-stack-static {
|
||||
.ui-resizable-handle {
|
||||
@@ -846,7 +851,7 @@ export default defineComponent({
|
||||
}
|
||||
.grid-stack-item-content {
|
||||
border: 1px solid #c2c2c27a;
|
||||
border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
border-radius: inherit;
|
||||
}
|
||||
@@ -855,7 +860,6 @@ export default defineComponent({
|
||||
/* GridStack theme overrides */
|
||||
:deep(.grid-stack) {
|
||||
.grid-stack-item {
|
||||
|
||||
.drag-allow {
|
||||
cursor: move;
|
||||
}
|
||||
@@ -869,10 +873,10 @@ export default defineComponent({
|
||||
&.ui-resizable-resizing {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
|
||||
> .ui-resizable-handle {
|
||||
background: none;
|
||||
|
||||
background: none;
|
||||
|
||||
&.ui-resizable-se {
|
||||
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M8 2 L8 8 L2 8' stroke='%23999999' stroke-width='1.5' fill='none' stroke-linecap='round'/></svg>")
|
||||
no-repeat center;
|
||||
|
||||
Reference in New Issue
Block a user