diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_dashboard.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_dashboard.vue
index 8994f1921e876344beac669fdad3e320784c9741..c91511e0131ae413f2ef7367b11b3dd9d4100ffe 100644
--- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_dashboard.vue
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_dashboard.vue
@@ -13,6 +13,7 @@ import {
   buildDefaultDashboardFilters,
   getDashboardConfig,
   updateApolloCache,
+  getUniquePanelId,
 } from 'ee/vue_shared/components/customizable_dashboard/utils';
 import { saveCustomDashboard } from 'ee/analytics/analytics_dashboards/api/dashboards_api';
 import {
@@ -101,6 +102,7 @@ export default {
       changesSaved: false,
       alert: null,
       hasDashboardError: false,
+      savedPanels: null,
     };
   },
   computed: {
@@ -170,7 +172,16 @@ export default {
 
         return {
           ...dashboard,
-          panels: dashboard.panels?.nodes || [],
+          panels:
+            // Panel ids need to remain consistent and they are unique to the
+            // frontend. Thus they don't get saved with GraphQL and we need to
+            // reference the saved panels array to persist the ids.
+            this.savedPanels ||
+            dashboard.panels?.nodes?.map((panel) => ({
+              ...panel,
+              id: getUniquePanelId(),
+            })) ||
+            [],
         };
       },
       result() {
@@ -269,6 +280,8 @@ export default {
             isGroup: this.isGroup,
           });
 
+          this.savedPanels = dashboard.panels;
+
           if (this.isNewDashboard) {
             // We redirect now to the new route
             this.$router.push({
diff --git a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue
index a8824d8549a3ae39ec45cac31b82e6c45b0a3f54..f50bb0ebc3868e08960fc9485f43bfa49971ed72 100644
--- a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue
+++ b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/customizable_dashboard.vue
@@ -106,7 +106,6 @@ export default {
       filters: this.defaultFilters,
       alert: null,
       visualizationDrawerOpen: false,
-      dashboardRenderKey: 0,
     };
   },
   computed: {
@@ -215,8 +214,6 @@ export default {
     },
     resetToInitialDashboard() {
       this.dashboard = this.createDraftDashboard(this.initialDashboard);
-      // Update the element key to force re-render
-      this.dashboardRenderKey += 1;
     },
     onTitleInput(submitting) {
       this.$emit('title-input', this.dashboard.title, submitting);
@@ -318,10 +315,15 @@ export default {
     panelTestId({ visualization: { slug = '' } }) {
       return `panel-${slug.replaceAll('_', '-')}`;
     },
-    createVisualizationPanels(visualizations) {
+    deletePanel(panel) {
+      const removeIndex = this.dashboard.panels.findIndex((p) => p.id === panel.id);
+      this.dashboard.panels.splice(removeIndex, 1);
+    },
+    addPanels(visualizations) {
       this.closeVisualizationDrawer();
 
-      return visualizations.map((viz) => createNewVisualizationPanel(viz));
+      const panels = visualizations.map((viz) => createNewVisualizationPanel(viz));
+      this.dashboard.panels.push(...panels);
     },
   },
   HISTORY_REPLACE_UPDATE_METHOD,
@@ -458,8 +460,8 @@ export default {
             </div>
           </button>
 
-          <gridstack-wrapper :key="dashboardRenderKey" v-model="dashboard" :editing="editing">
-            <template #panel="{ panel, deletePanel }">
+          <gridstack-wrapper v-model="dashboard" :editing="editing">
+            <template #panel="{ panel }">
               <panels-base
                 :title="panel.title"
                 :visualization="panel.visualization"
@@ -470,17 +472,16 @@ export default {
                 @delete="deletePanel(panel)"
               />
             </template>
-            <template #drawer="{ addPanels }">
-              <available-visualizations-drawer
-                :visualizations="availableVisualizations.visualizations"
-                :loading="availableVisualizations.loading"
-                :has-error="availableVisualizations.hasError"
-                :open="visualizationDrawerOpen"
-                @select="(visualizations) => addPanels(createVisualizationPanels(visualizations))"
-                @close="closeVisualizationDrawer"
-              />
-            </template>
           </gridstack-wrapper>
+
+          <available-visualizations-drawer
+            :visualizations="availableVisualizations.visualizations"
+            :loading="availableVisualizations.loading"
+            :has-error="availableVisualizations.hasError"
+            :open="visualizationDrawerOpen"
+            @select="addPanels"
+            @close="closeVisualizationDrawer"
+          />
         </div>
       </div>
     </div>
diff --git a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/gridstack_wrapper.vue b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/gridstack_wrapper.vue
index 28165c9f7e9d10625270228c2f89bf1c4b2e4de9..c69ac3d463067961a6abf753cf7c34096e62475d 100644
--- a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/gridstack_wrapper.vue
+++ b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/gridstack_wrapper.vue
@@ -11,7 +11,7 @@ import {
   GRIDSTACK_MIN_ROW,
   CURSOR_GRABBING_CLASS,
 } from './constants';
-import { getUniquePanelId } from './utils';
+import { parsePanelToGridItem } from './utils';
 
 export default {
   name: 'GridstackWrapper',
@@ -28,16 +28,19 @@ export default {
   },
   data() {
     return {
-      dashboard: this.createDraftDashboard(this.value),
       grid: undefined,
       cssLoaded: false,
       mounted: false,
+      gridPanels: [],
     };
   },
   computed: {
     mountedWithCss() {
       return this.cssLoaded && this.mounted;
     },
+    gridConfig() {
+      return this.value.panels.map(parsePanelToGridItem);
+    },
   },
   watch: {
     mountedWithCss(mountedWithCss) {
@@ -48,6 +51,12 @@ export default {
     editing(value) {
       this.grid?.setStatic(!value);
     },
+    gridConfig: {
+      handler(config) {
+        this.grid?.load(config);
+      },
+      deep: true,
+    },
   },
   mounted() {
     this.mounted = true;
@@ -65,16 +74,34 @@ export default {
     }
   },
   methods: {
-    createDraftDashboard(dashboard) {
-      const draft = cloneWithoutReferences(dashboard);
-      return {
-        ...draft,
-        // Gridstack requires unique panel IDs for mutations
-        panels: draft.panels.map((panel) => ({
-          ...panel,
-          id: getUniquePanelId(),
-        })),
-      };
+    async mountGridComponents(panels, options = { scollIntoView: false }) {
+      // Ensure new panels are always rendered first
+      await this.$nextTick();
+
+      panels.forEach((panel) => {
+        const wrapper = this.$refs.panelWrappers.find((w) => w.id === panel.id);
+        const widgetContentEl = panel.el.querySelector('.grid-stack-item-content');
+
+        widgetContentEl.appendChild(wrapper);
+      });
+
+      if (options.scrollIntoView) {
+        const mostRecent = panels[panels.length - 1];
+        mostRecent.el.scrollIntoView({ behavior: 'smooth' });
+      }
+    },
+    getGridItemForElement(el) {
+      return this.gridConfig.find((item) => item.id === el.getAttribute('gs-id'));
+    },
+    initGridPanelSlots(gridElements) {
+      if (!gridElements) return;
+
+      this.gridPanels = gridElements.map((el) => ({
+        ...this.getGridItemForElement(el),
+        el,
+      }));
+
+      this.mountGridComponents(this.gridPanels);
     },
     initGridStack() {
       this.grid = GridStack.init({
@@ -86,7 +113,10 @@ export default {
         columnOpts: { breakpoints: [{ w: breakpoints.md, c: 1 }] },
         alwaysShowResizeHandle: true,
         animate: false,
-      });
+      }).load(this.gridConfig);
+
+      // Sync Vue components array with gridstack items
+      this.initGridPanelSlots(this.grid.getGridItems());
 
       this.grid.on('dragstart', () => {
         this.$el.classList.add(CURSOR_GRABBING_CLASS);
@@ -95,91 +125,66 @@ export default {
         this.$el.classList.remove(CURSOR_GRABBING_CLASS);
       });
       this.grid.on('change', (_, items) => {
-        items.forEach((item) => {
-          this.updatePanelWithGridStackItem(item);
-        });
+        if (!items) return;
+
+        this.emitLayoutChanges(items);
       });
       this.grid.on('added', (_, items) => {
-        items.forEach((item) => {
-          this.updatePanelWithGridStackItem(item);
-        });
+        this.addGridPanels(items);
+      });
+      this.grid.on('removed', (_, items) => {
+        this.removeGridPanels(items);
       });
     },
-    registerNewGridPanelElement(panelId) {
-      this.grid.makeWidget(`#${panelId}`);
-
-      document.getElementById(panelId)?.scrollIntoView({ behavior: 'smooth' });
-    },
-    getGridAttribute(panel, attribute) {
-      const { gridAttributes = {} } = panel;
-
-      return gridAttributes[attribute];
+    convertToGridAttributes(gridStackItem) {
+      return {
+        yPos: gridStackItem.y,
+        xPos: gridStackItem.x,
+        width: gridStackItem.w,
+        height: gridStackItem.h,
+      };
     },
-    deletePanel(panel) {
-      const panelIndex = this.dashboard.panels.indexOf(panel);
-      this.dashboard.panels.splice(panelIndex, 1);
-
-      this.grid.removeWidget(document.getElementById(panel.id), false);
-
-      this.emitChange();
+    removeGridPanels(items) {
+      items.forEach((item) => {
+        const index = this.gridPanels.findIndex((c) => c.id === item.id);
+        this.gridPanels.splice(index, 1);
+        // Finally remove the gridstack element
+        item.el.remove();
+      });
     },
-    async addPanels(panels) {
-      const panelIds = panels.map(({ id }) => id);
-
-      this.dashboard.panels.push(...panels);
+    addGridPanels(items) {
+      const newPanels = items.map(({ grid, ...rest }) => ({ ...rest }));
+      this.gridPanels.push(...newPanels);
 
-      // Wait for the panel elements to render
-      await this.$nextTick();
-
-      panelIds.forEach((id) => this.registerNewGridPanelElement(id));
-
-      this.emitChange();
-    },
-    updatePanelWithGridStackItem(item) {
-      const updatedPanel = this.dashboard.panels.find((panel) => panel.id === item.id);
-      if (updatedPanel) {
-        updatedPanel.gridAttributes = this.convertToGridAttributes(item);
-        this.emitChange();
-      }
+      this.mountGridComponents(newPanels, { scollIntoView: true });
     },
-    emitChange() {
-      this.$emit('input', this.dashboard);
-    },
-    convertToGridAttributes(gridStackProperties) {
-      return {
-        yPos: gridStackProperties.y,
-        xPos: gridStackProperties.x,
-        width: gridStackProperties.w,
-        height: gridStackProperties.h,
-      };
+    emitLayoutChanges(items) {
+      const newValue = cloneWithoutReferences(this.value);
+      items.forEach((item) => {
+        const panel = newValue.panels.find((p) => p.id === item.id);
+        panel.gridAttributes = {
+          ...panel.gridAttributes,
+          ...this.convertToGridAttributes(item),
+        };
+      });
+      this.$emit('input', newValue);
     },
   },
 };
 </script>
 
 <template>
-  <div>
-    <div class="grid-stack" data-testid="gridstack-grid">
-      <div
-        v-for="panel in dashboard.panels"
-        :id="panel.id"
-        :key="panel.id"
-        :gs-id="panel.id"
-        :gs-x="getGridAttribute(panel, 'xPos')"
-        :gs-y="getGridAttribute(panel, 'yPos')"
-        :gs-h="getGridAttribute(panel, 'height')"
-        :gs-w="getGridAttribute(panel, 'width')"
-        :gs-min-h="getGridAttribute(panel, 'minHeight')"
-        :gs-min-w="getGridAttribute(panel, 'minWidth')"
-        :gs-max-h="getGridAttribute(panel, 'maxHeight')"
-        :gs-max-w="getGridAttribute(panel, 'maxWidth')"
-        class="grid-stack-item"
-        :class="{ 'gl-cursor-grab': editing }"
-        data-testid="grid-stack-panel"
-      >
-        <slot name="panel" v-bind="{ panel, editing, deletePanel }"></slot>
-      </div>
+  <div class="grid-stack" data-testid="gridstack-grid">
+    <div
+      v-for="panel in gridPanels"
+      :id="panel.id"
+      ref="panelWrappers"
+      :key="panel.id"
+      class="gl-h-full"
+      :class="{ 'gl-cursor-grab': editing }"
+      data-testid="grid-stack-panel"
+    >
+      <slot name="panel" v-bind="{ panel: panel.props }"></slot>
     </div>
-    <slot name="drawer" v-bind="{ addPanels }"></slot>
   </div>
 </template>
diff --git a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/panels_base.vue b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/panels_base.vue
index b3c7c4162981ef4efe948d4c4ffb47824db66a58..0d011d2c181b39da17ab2734760e94c0371f4331 100644
--- a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/panels_base.vue
+++ b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/panels_base.vue
@@ -212,7 +212,7 @@ export default {
 <template>
   <div
     :id="popoverId"
-    class="grid-stack-item-content gl-border gl-rounded-base gl-p-4 gl-bg-white gl-overflow-visible!"
+    class="grid-stack-item-content gl-border gl-rounded-base gl-p-4 gl-bg-white gl-overflow-visible! gl-h-full"
     :class="{
       'gl-border-t-2 gl-border-t-solid gl-border-t-red-500': showErrorState,
     }"
@@ -254,6 +254,7 @@ export default {
           fluid-width
           toggle-class="gl-ml-1"
           category="tertiary"
+          positioning-strategy="fixed"
         >
           <template #list-item="{ item }">
             <span :data-testId="`panel-action-${item.testId}`">
@@ -296,6 +297,7 @@ export default {
         :css-classes="['gl-max-w-50p']"
         :target="popoverId"
         :delay="$options.PANEL_POPOVER_DELAY"
+        boundary="viewport"
       >
         <gl-sprintf :message="errorPopoverMessage">
           <template #link="{ content }">
diff --git a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/utils.js b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/utils.js
index 68f81d20ffe6a1ce0c537f8af9d682f600b0d574..28136f66e1fc6084006b8717696da5e94cd7073b 100644
--- a/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/utils.js
+++ b/ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/utils.js
@@ -242,3 +242,33 @@ export const updateApolloCache = ({
   });
   updateDashboardsListApolloCache({ apolloClient, slug, dashboard, fullPath, isProject, isGroup });
 };
+
+const filterUndefinedValues = (obj) => {
+  // eslint-disable-next-line no-unused-vars
+  return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined));
+};
+
+/**
+ * Parses a dashboard panel config into a GridStack item.
+ */
+export const parsePanelToGridItem = ({
+  gridAttributes: { xPos, yPos, width, height, minHeight, minWidth, maxHeight, maxWidth },
+  id,
+  ...rest
+}) =>
+  // GridStack renders undefined layout values so we need to filter them out.
+  filterUndefinedValues({
+    x: xPos,
+    y: yPos,
+    w: width,
+    h: height,
+    minH: minHeight,
+    minW: minWidth,
+    maxH: maxHeight,
+    maxW: maxWidth,
+    id,
+    props: {
+      id,
+      ...rest,
+    },
+  });
diff --git a/ee/spec/frontend/analytics/analytics_dashboards/components/analytics_dashboard_spec.js b/ee/spec/frontend/analytics/analytics_dashboards/components/analytics_dashboard_spec.js
index f85924e935a4cdacf672988dc3f44debec9c2e02..73dfbc099f64e18b0b30d92fe6302f62c499e31c 100644
--- a/ee/spec/frontend/analytics/analytics_dashboards/components/analytics_dashboard_spec.js
+++ b/ee/spec/frontend/analytics/analytics_dashboards/components/analytics_dashboard_spec.js
@@ -244,6 +244,20 @@ describe('AnalyticsDashboard', () => {
 
       expect(findDashboard().exists()).toBe(true);
     });
+
+    it('should add unique panel ids to each panel', async () => {
+      createWrapper();
+
+      await waitForPromises();
+
+      expect(findDashboard().props().initialDashboard.panels).toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({
+            id: expect.stringContaining('panel-'),
+          }),
+        ]),
+      );
+    });
   });
 
   describe('when dashboard fails to load', () => {
@@ -422,7 +436,15 @@ describe('AnalyticsDashboard', () => {
       });
 
       describe('with a valid dashboard', () => {
-        beforeEach(() => mockSaveDashboardImplementation(() => ({ status: HTTP_STATUS_CREATED })));
+        let originalPanels;
+
+        beforeEach(async () => {
+          await waitForPromises();
+
+          originalPanels = findDashboard().props().initialDashboard.panels;
+
+          await mockSaveDashboardImplementation(() => ({ status: HTTP_STATUS_CREATED }));
+        });
 
         it('saves the dashboard and shows a success toast', () => {
           expect(saveCustomDashboard).toHaveBeenCalledWith({
@@ -449,6 +471,10 @@ describe('AnalyticsDashboard', () => {
             expect.any(Object),
           );
         });
+
+        it('persists the original panels array after saving', () => {
+          expect(findDashboard().props().initialDashboard.panels).toStrictEqual(originalPanels);
+        });
       });
 
       describe('with an invalid dashboard', () => {
@@ -590,8 +616,14 @@ describe('AnalyticsDashboard', () => {
       });
 
       describe('when saving', () => {
-        beforeEach(() => {
-          return mockSaveDashboardImplementation(() => ({ status: HTTP_STATUS_CREATED }));
+        let originalPanels;
+
+        beforeEach(async () => {
+          await waitForPromises();
+
+          originalPanels = findDashboard().props().initialDashboard.panels;
+
+          await mockSaveDashboardImplementation(() => ({ status: HTTP_STATUS_CREATED }));
         });
 
         it('saves the dashboard as a new file', () => {
@@ -613,6 +645,10 @@ describe('AnalyticsDashboard', () => {
             expect.any(Object),
           );
         });
+
+        it('persists the original panels array after saving', () => {
+          expect(findDashboard().props().initialDashboard.panels).toStrictEqual(originalPanels);
+        });
       });
     });
   });
diff --git a/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js b/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js
index d329cdc467f109ce8be647e06d4dc41636de1a9e..ca0ac3b8c335f5e4bd9c5a63ad07881ee7c1bf00 100644
--- a/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js
+++ b/ee/spec/frontend/vue_shared/components/customizable_dashboard/customizable_dashboard_spec.js
@@ -415,24 +415,6 @@ describe('CustomizableDashboard', () => {
             },
           );
         });
-
-        it.each`
-          action                | confirmed | expectedKey
-          ${'confirm discard'}  | ${true}   | ${1}
-          ${'continue editing'} | ${false}  | ${0}
-        `(
-          'sets the gridstack-wrapper render key to "$expectedKey" on $action',
-          async ({ confirmed, expectedKey }) => {
-            confirmAction.mockResolvedValue(confirmed);
-
-            expect(findGridstackWrapper().vm.$vnode.key).toBe(0);
-
-            await findCancelButton().vm.$emit('click');
-            await waitForPromises();
-
-            expect(findGridstackWrapper().vm.$vnode.key).toBe(expectedKey);
-          },
-        );
       });
     });
 
@@ -485,26 +467,31 @@ describe('CustomizableDashboard', () => {
           expect(findVisualizationDrawer().props('open')).toBe(false);
         });
 
-        it('calls the wrapper method to add new panels', () => {
-          expect(addPanelsMock).toHaveBeenCalledWith(
-            expect.arrayContaining([
-              expect.objectContaining({
-                ...createNewVisualizationPanel(TEST_VISUALIZATION()),
-                id: expect.stringContaining('panel-'),
-              }),
-            ]),
-          );
+        it('adds new panels to the dashboard', () => {
+          const { panels } = findGridstackWrapper().props().value;
+
+          expect(panels).toHaveLength(3);
+          expect(panels[2]).toMatchObject({
+            ...createNewVisualizationPanel(TEST_VISUALIZATION()),
+            id: expect.stringContaining('panel-'),
+          });
         });
       });
     });
 
     describe('add a panel is deleted', () => {
+      const removePanel = dashboard.panels[0];
+
       beforeEach(async () => {
-        await findPanels().at(0).vm.$emit('delete', dashboard.panels[0]);
+        await findPanels().at(0).vm.$emit('delete', removePanel);
       });
 
-      it('calls the wrapper method to delete the panel', () => {
-        expect(deletePanelMock).toHaveBeenCalledWith(dashboard.panels[0]);
+      it('removes the chosen panel from the dashboard', () => {
+        const { panels } = findGridstackWrapper().props().value;
+        const panelIds = panels.map(({ id }) => id);
+
+        expect(panels).toHaveLength(1);
+        expect(panelIds).not.toContain(removePanel.id);
       });
     });
   });
diff --git a/ee/spec/frontend/vue_shared/components/customizable_dashboard/gridstack_wrapper_spec.js b/ee/spec/frontend/vue_shared/components/customizable_dashboard/gridstack_wrapper_spec.js
index 44b192705c2ef1405a2ed8fc83a480ef17e9e696..fa0387a64ecd41e64e423b03961652f55f1ea8ab 100644
--- a/ee/spec/frontend/vue_shared/components/customizable_dashboard/gridstack_wrapper_spec.js
+++ b/ee/spec/frontend/vue_shared/components/customizable_dashboard/gridstack_wrapper_spec.js
@@ -12,24 +12,29 @@ import {
 } from 'ee/vue_shared/components/customizable_dashboard/constants';
 import { loadCSSFile } from '~/lib/utils/css_utils';
 import waitForPromises from 'helpers/wait_for_promises';
+import { parsePanelToGridItem } from 'ee/vue_shared/components/customizable_dashboard/utils';
 import { createNewVisualizationPanel } from 'ee/analytics/analytics_dashboards/utils';
 import { dashboard, builtinDashboard } from './mock_data';
 
 const mockGridSetStatic = jest.fn();
 const mockGridDestroy = jest.fn();
-jest.mock('gridstack', () => ({
-  GridStack: {
-    init: jest.fn(() => {
-      return {
-        on: jest.fn(),
-        destroy: mockGridDestroy,
-        makeWidget: jest.fn(),
-        setStatic: mockGridSetStatic,
-        removeWidget: jest.fn(),
-      };
-    }),
-  },
-}));
+const mockGridLoad = jest.fn();
+
+jest.mock('gridstack', () => {
+  const actualModule = jest.requireActual('gridstack');
+
+  return {
+    GridStack: {
+      init: jest.fn().mockImplementation((config) => {
+        const instance = actualModule.GridStack.init(config);
+        instance.load = mockGridLoad.mockImplementation(instance.load);
+        instance.setStatic = mockGridSetStatic;
+        instance.destroy = mockGridDestroy;
+        return instance;
+      }),
+    },
+  };
+});
 
 jest.mock('~/lib/utils/css_utils', () => ({
   loadCSSFile: jest.fn(),
@@ -39,7 +44,6 @@ describe('GridstackWrapper', () => {
   /** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
   let wrapper;
   let panelSlots = [];
-  let drawerSlot;
 
   const createWrapper = (props = {}) => {
     wrapper = shallowMountExtended(GridstackWrapper, {
@@ -51,14 +55,14 @@ describe('GridstackWrapper', () => {
         panel(data) {
           panelSlots.push(data);
         },
-        drawer(data) {
-          drawerSlot = data;
-        },
       },
+      attachTo: document.body,
     });
   };
 
   const findGridStackPanels = () => wrapper.findAllByTestId('grid-stack-panel');
+  const findGridItemContentById = (panelId) =>
+    wrapper.find(`[gs-id="${panelId}"]`).find('.grid-stack-item-content');
   const findPanelById = (panelId) => wrapper.find(`#${panelId}`);
 
   afterEach(() => {
@@ -87,14 +91,51 @@ describe('GridstackWrapper', () => {
       });
     });
 
-    it('does not render the grab cursor on grid panels', () => {
+    it('loads the parsed dashboard config', () => {
+      expect(mockGridLoad).toHaveBeenCalledWith(dashboard.panels.map(parsePanelToGridItem));
+    });
+
+    it('does not render a the grab cursor on grid panels', () => {
       expect(findGridStackPanels().at(0).classes()).not.toContain('gl-cursor-grab');
     });
 
-    it('passes data to drawer slot', () => {
-      expect(drawerSlot).toStrictEqual({
-        addPanels: expect.any(Function),
+    it('renders a panel once it has been added', async () => {
+      const newPanel = createNewVisualizationPanel(builtinDashboard.panels[0].visualization);
+
+      expect(findPanelById(newPanel.id).exists()).toBe(false);
+
+      wrapper.setProps({
+        value: {
+          ...dashboard,
+          panels: [...dashboard.panels, newPanel],
+        },
       });
+
+      await waitForPromises();
+
+      const gridItem = findGridItemContentById(newPanel.id);
+      const panel = findPanelById(newPanel.id);
+
+      expect(panel.element.parentElement).toBe(gridItem.element);
+    });
+
+    it('does not render a removed panel', async () => {
+      const panelToRemove = dashboard.panels[0];
+
+      expect(findGridStackPanels()).toHaveLength(dashboard.panels.length);
+      expect(findPanelById(panelToRemove.id).exists()).toBe(true);
+
+      wrapper.setProps({
+        value: {
+          ...dashboard,
+          panels: dashboard.panels.filter((panel) => panel.id !== panelToRemove.id),
+        },
+      });
+
+      await waitForPromises();
+
+      expect(findGridStackPanels()).toHaveLength(dashboard.panels.length - 1);
+      expect(findPanelById(panelToRemove.id).exists()).toBe(false);
     });
 
     describe.each(dashboard.panels.map((panel, index) => [panel, index]))(
@@ -103,22 +144,21 @@ describe('GridstackWrapper', () => {
         it('renders a grid panel', () => {
           const element = findGridStackPanels().at(index);
 
-          expect(element.attributes()).toMatchObject({
-            'gs-id': expect.stringContaining('panel-'),
-            'gs-h': `${panel.gridAttributes.height}`,
-            'gs-w': `${panel.gridAttributes.width}`,
-          });
+          expect(element.attributes().id).toContain('panel-');
         });
 
-        it('passes data to the panel slot', () => {
-          expect(panelSlots[index]).toStrictEqual({
-            panel: {
-              ...dashboard.panels[index],
-              id: expect.stringContaining('panel-'),
-            },
-            editing: false,
-            deletePanel: expect.any(Function),
-          });
+        it('sets the panel props on the panel slot', () => {
+          const { gridAttributes, ...panelProps } = panel;
+
+          expect(panelSlots[index]).toStrictEqual({ panel: panelProps });
+        });
+
+        it("renders the panel inside the grid item's content", async () => {
+          const gridItem = findGridItemContentById(panel.id);
+
+          await nextTick();
+
+          expect(findGridStackPanels().at(index).element.parentElement).toBe(gridItem.element);
         });
       },
     );
@@ -153,49 +193,46 @@ describe('GridstackWrapper', () => {
     });
   });
 
-  describe('when a panel is updated', () => {
-    let gridPanel;
-
-    beforeEach(() => {
+  describe('when the grid changes', () => {
+    beforeEach(async () => {
       loadCSSFile.mockResolvedValue();
       createWrapper();
 
-      gridPanel = findGridStackPanels().at(0);
+      await waitForPromises();
 
-      wrapper.vm.updatePanelWithGridStackItem({
-        id: gridPanel.attributes('id'),
-        x: 10,
-        y: 20,
-        w: 30,
-        h: 40,
+      const gridEl = wrapper.find('.grid-stack').element;
+      const event = new CustomEvent('change', {
+        detail: [
+          {
+            id: dashboard.panels[1].id,
+            x: 10,
+            y: 20,
+            w: 30,
+            h: 40,
+          },
+        ],
       });
-    });
 
-    it('updates the panels grid attributes', () => {
-      expect(gridPanel.attributes()).toMatchObject({
-        'gs-h': '40',
-        'gs-w': '30',
-        'gs-x': '10',
-        'gs-y': '20',
-      });
+      gridEl.dispatchEvent(event);
     });
 
     it('emits the changed dashboard object', () => {
-      expect(wrapper.emitted('input')).toMatchObject([
+      expect(wrapper.emitted('input')).toStrictEqual([
         [
           {
             ...dashboard,
             panels: [
+              dashboard.panels[0],
               {
-                ...dashboard.panels[0],
+                ...dashboard.panels[1],
                 gridAttributes: {
+                  ...dashboard.panels[1].gridAttributes,
                   xPos: 10,
                   yPos: 20,
                   width: 30,
                   height: 40,
                 },
               },
-              ...dashboard.panels.slice(1),
             ],
           },
         ],
@@ -203,78 +240,6 @@ describe('GridstackWrapper', () => {
     });
   });
 
-  describe('when panels are added', () => {
-    let newPanel;
-
-    beforeEach(() => {
-      loadCSSFile.mockResolvedValue();
-      createWrapper();
-
-      newPanel = createNewVisualizationPanel(builtinDashboard.panels[0].visualization);
-    });
-
-    it('adds panels to the dashboard', async () => {
-      expect(findGridStackPanels().length).toEqual(2);
-      expect(findPanelById(newPanel.id).exists()).toBe(false);
-
-      drawerSlot.addPanels([newPanel]);
-      await nextTick();
-
-      expect(findGridStackPanels().length).toEqual(3);
-      expect(findPanelById(newPanel.id).exists()).toBe(true);
-    });
-
-    it('emits the changed dashboard object', async () => {
-      drawerSlot.addPanels([newPanel]);
-      await nextTick();
-
-      expect(wrapper.emitted('input')).toMatchObject([
-        [
-          {
-            ...dashboard,
-            panels: [...dashboard.panels, newPanel],
-          },
-        ],
-      ]);
-    });
-  });
-
-  describe('when a panel is deleted', () => {
-    let removePanel;
-
-    beforeEach(() => {
-      loadCSSFile.mockResolvedValue();
-      createWrapper();
-
-      removePanel = panelSlots[0].panel;
-    });
-
-    it('should remove the panel from the dashboard', async () => {
-      expect(findGridStackPanels().length).toEqual(2);
-      expect(findPanelById(removePanel.id).exists()).toBe(true);
-
-      panelSlots[0].deletePanel(removePanel);
-      await nextTick();
-
-      expect(findGridStackPanels().length).toEqual(1);
-      expect(findPanelById(removePanel.id).exists()).toBe(false);
-    });
-
-    it('emits the changed dashboard object', async () => {
-      panelSlots[0].deletePanel(removePanel);
-      await nextTick();
-
-      expect(wrapper.emitted('input')).toMatchObject([
-        [
-          {
-            ...dashboard,
-            panels: dashboard.panels.slice(1),
-          },
-        ],
-      ]);
-    });
-  });
-
   describe('when an error occurs while loading the CSS', () => {
     const sentryError = new Error('Network error');
 
diff --git a/ee/spec/frontend/vue_shared/components/customizable_dashboard/mock_data.js b/ee/spec/frontend/vue_shared/components/customizable_dashboard/mock_data.js
index e3658434d14e53621e37fdeca7adfea97c8c837f..55d1d62fe00b6c29a5c0084e1a6b5155fb947afc 100644
--- a/ee/spec/frontend/vue_shared/components/customizable_dashboard/mock_data.js
+++ b/ee/spec/frontend/vue_shared/components/customizable_dashboard/mock_data.js
@@ -1,4 +1,5 @@
 import { __ } from '~/locale';
+import { getUniquePanelId } from 'ee/vue_shared/components/customizable_dashboard/utils';
 
 const cubeLineChart = {
   type: 'LineChart',
@@ -36,14 +37,16 @@ export const dashboard = {
       gridAttributes: { width: 3, height: 3 },
       visualization: cubeLineChart,
       queryOverrides: null,
+      id: getUniquePanelId(),
     },
     {
       title: __('Test B'),
-      gridAttributes: { width: 2, height: 4 },
+      gridAttributes: { width: 2, height: 4, minHeight: 2, minWidth: 2 },
       visualization: cubeLineChart,
       queryOverrides: {
         limit: 200,
       },
+      id: getUniquePanelId(),
     },
   ],
 };
@@ -77,6 +80,7 @@ export const builtinDashboard = {
       gridAttributes: { width: 3, height: 3 },
       visualization: cubeLineChart,
       queryOverrides: {},
+      id: getUniquePanelId(),
     },
   ],
 };
@@ -86,3 +90,20 @@ export const mockDateRangeFilterChangePayload = {
   endDate: new Date('2016-02-01'),
   dateRangeOption: 'foo',
 };
+
+export const mockPanel = {
+  title: __('Test A'),
+  gridAttributes: {
+    width: 1,
+    height: 2,
+    xPos: 0,
+    yPos: 3,
+    minWidth: 1,
+    minHeight: 2,
+    maxWidth: 1,
+    maxHeight: 2,
+  },
+  visualization: cubeLineChart,
+  queryOverrides: {},
+  id: getUniquePanelId(),
+};
diff --git a/ee/spec/frontend/vue_shared/components/customizable_dashboard/utils_spec.js b/ee/spec/frontend/vue_shared/components/customizable_dashboard/utils_spec.js
index f2d25f7cb17615682881d5a5f25115215dce131a..46056736902c3107e6f3059b8830d51878afd586 100644
--- a/ee/spec/frontend/vue_shared/components/customizable_dashboard/utils_spec.js
+++ b/ee/spec/frontend/vue_shared/components/customizable_dashboard/utils_spec.js
@@ -8,6 +8,7 @@ import {
   getDashboardConfig,
   updateApolloCache,
   getVisualizationCategory,
+  parsePanelToGridItem,
 } from 'ee/vue_shared/components/customizable_dashboard/utils';
 import { parsePikadayDate } from '~/lib/utils/datetime_utility';
 import {
@@ -30,7 +31,7 @@ import {
   TEST_CUSTOM_DASHBOARD_GRAPHQL_SUCCESS_RESPONSE,
   getGraphQLDashboard,
 } from 'ee_jest/analytics/analytics_dashboards/mock_data';
-import { mockDateRangeFilterChangePayload, dashboard } from './mock_data';
+import { mockDateRangeFilterChangePayload, dashboard, mockPanel } from './mock_data';
 
 const option = DATE_RANGE_OPTIONS[0];
 
@@ -406,3 +407,29 @@ describe('getVisualizationCategory', () => {
     expect(getVisualizationCategory({ type })).toBe(category);
   });
 });
+
+describe('parsePanelToGridItem', () => {
+  it('parses all panel configs to GridStack format', () => {
+    const { gridAttributes, ...rest } = mockPanel;
+
+    expect(parsePanelToGridItem(mockPanel)).toStrictEqual({
+      x: gridAttributes.xPos,
+      y: gridAttributes.yPos,
+      w: gridAttributes.width,
+      h: gridAttributes.height,
+      minH: gridAttributes.minHeight,
+      minW: gridAttributes.minWidth,
+      maxH: gridAttributes.maxHeight,
+      maxW: gridAttributes.maxWidth,
+      id: mockPanel.id,
+      props: rest,
+    });
+  });
+
+  it('filters out props with undefined values', () => {
+    const local = { ...mockPanel };
+    local.id = undefined;
+
+    expect(Object.keys(parsePanelToGridItem(local))).not.toContain('id');
+  });
+});