Repository: lightdash/lightdash
Pull request #14438: feat: refreshed delete space modal
Author: login: notgiorgi
Labels: name: released
Created: 2025-04-16T12:55:12Z | Merged: 2025-04-17T12:47:33Z

<!-- Thanks so much for your PR, your contribution is appreciated! ❤️ -->

Closes: #14327

### Description:
<!-- Add a description of the changes proposed in the pull request. -->

![CleanShot 2025-04-16 at 16.57.33@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/SRLEqMEevAzoFwvhnfeq/90ba6d87-045e-4bfa-9d70-443881785af2.png)

![CleanShot 2025-04-16 at 16.57.46@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/SRLEqMEevAzoFwvhnfeq/8cb98b4d-ca13-4569-937c-48a8748bca66.png)

![CleanShot 2025-04-16 at 16.57.58@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/SRLEqMEevAzoFwvhnfeq/8b68e305-d1f6-40b0-8fa9-3b059a7d0396.png)

<!-- Even better add a screenshot / gif / loom -->

### Reviewer actions

- [ ] I have manually tested the changes in the preview environment
- [ ] I have reviewed the code
- [ ] I understand that "request changes" will block this PR from merging



Diff:
diff --git a/packages/backend/src/generated/routes.ts b/packages/backend/src/generated/routes.ts
index b61125144c157..0fb4783f06971 100644
--- a/packages/backend/src/generated/routes.ts
+++ b/packages/backend/src/generated/routes.ts
@@ -6708,6 +6708,60 @@ const models: TsoaRoute.Models = {
         },
     },
     // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+    'Pick_SpaceSummary.Exclude_keyofSpaceSummary.userAccess__': {
+        dataType: 'refAlias',
+        type: {
+            dataType: 'nestedObjectLiteral',
+            nestedProperties: {
+                name: { dataType: 'string', required: true },
+                projectUuid: { dataType: 'string', required: true },
+                uuid: { dataType: 'string', required: true },
+                organizationUuid: { dataType: 'string', required: true },
+                isPrivate: { dataType: 'boolean', required: true },
+                pinnedListUuid: {
+                    dataType: 'union',
+                    subSchemas: [
+                        { dataType: 'string' },
+                        { dataType: 'enum', enums: [null] },
+                    ],
+                    required: true,
+                },
+                pinnedListOrder: {
+                    dataType: 'union',
+                    subSchemas: [
+                        { dataType: 'double' },
+                        { dataType: 'enum', enums: [null] },
+                    ],
+                    required: true,
+                },
+                slug: { dataType: 'string', required: true },
+                parentSpaceUuid: {
+                    dataType: 'union',
+                    subSchemas: [
+                        { dataType: 'string' },
+                        { dataType: 'undefined' },
+                    ],
+                },
+                access: {
+                    dataType: 'array',
+                    array: { dataType: 'string' },
+                    required: true,
+                },
+                chartCount: { dataType: 'double', required: true },
+                dashboardCount: { dataType: 'double', required: true },
+            },
+            validators: {},
+        },
+    },
+    // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+    'Omit_SpaceSummary.userAccess_': {
+        dataType: 'refAlias',
+        type: {
+            ref: 'Pick_SpaceSummary.Exclude_keyofSpaceSummary.userAccess__',
+            validators: {},
+        },
+    },
+    // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
     Space: {
         dataType: 'refAlias',
         type: {
@@ -6723,6 +6777,14 @@ const models: TsoaRoute.Models = {
                         },
                     },
                 },
+                childSpaces: {
+                    dataType: 'array',
+                    array: {
+                        dataType: 'refAlias',
+                        ref: 'Omit_SpaceSummary.userAccess_',
+                    },
+                    required: true,
+                },
                 parentSpaceUuid: { dataType: 'string' },
                 slug: { dataType: 'string', required: true },
                 pinnedListOrder: {
@@ -10400,7 +10462,7 @@ const models: TsoaRoute.Models = {
         },
     },
     // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
-    'PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__':
+    'PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__':
         {
             dataType: 'refAlias',
             type: {
@@ -10410,7 +10472,7 @@ const models: TsoaRoute.Models = {
             },
         },
     // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
-    'PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__':
+    'PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__':
         {
             dataType: 'refAlias',
             type: {
@@ -10420,7 +10482,7 @@ const models: TsoaRoute.Models = {
                         dataType: 'union',
                         subSchemas: [
                             {
-                                ref: 'PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__',
+                                ref: 'PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__',
                             },
                             { dataType: 'undefined' },
                         ],
@@ -10433,7 +10495,7 @@ const models: TsoaRoute.Models = {
     'PartialDeep_DashboardAsCodeLanguageMap._recurseIntoArrays-true__': {
         dataType: 'refAlias',
         type: {
-            ref: 'PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__',
+            ref: 'PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__',
             validators: {},
         },
     },
diff --git a/packages/backend/src/generated/swagger.json b/packages/backend/src/generated/swagger.json
index e21217a402f1b..68630d2d7a77f 100644
--- a/packages/backend/src/generated/swagger.json
+++ b/packages/backend/src/generated/swagger.json
@@ -7402,6 +7402,73 @@
                 "required": ["spaceRole", "groupName", "groupUuid"],
                 "type": "object"
             },
+            "Pick_SpaceSummary.Exclude_keyofSpaceSummary.userAccess__": {
+                "properties": {
+                    "name": {
+                        "type": "string"
+                    },
+                    "projectUuid": {
+                        "type": "string"
+                    },
+                    "uuid": {
+                        "type": "string"
+                    },
+                    "organizationUuid": {
+                        "type": "string"
+                    },
+                    "isPrivate": {
+                        "type": "boolean"
+                    },
+                    "pinnedListUuid": {
+                        "type": "string",
+                        "nullable": true
+                    },
+                    "pinnedListOrder": {
+                        "type": "number",
+                        "format": "double",
+                        "nullable": true
+                    },
+                    "slug": {
+                        "type": "string"
+                    },
+                    "parentSpaceUuid": {
+                        "type": "string"
+                    },
+                    "access": {
+                        "items": {
+                            "type": "string"
+                        },
+                        "type": "array"
+                    },
+                    "chartCount": {
+                        "type": "number",
+                        "format": "double"
+                    },
+                    "dashboardCount": {
+                        "type": "number",
+                        "format": "double"
+                    }
+                },
+                "required": [
+                    "name",
+                    "projectUuid",
+                    "uuid",
+                    "organizationUuid",
+                    "isPrivate",
+                    "pinnedListUuid",
+                    "pinnedListOrder",
+                    "slug",
+                    "access",
+                    "chartCount",
+                    "dashboardCount"
+                ],
+                "type": "object",
+                "description": "From T, pick a set of properties whose keys are in the union K"
+            },
+            "Omit_SpaceSummary.userAccess_": {
+                "$ref": "#/components/schemas/Pick_SpaceSummary.Exclude_keyofSpaceSummary.userAccess__",
+                "description": "Construct a type with the properties of T except for those in type K."
+            },
             "Space": {
                 "properties": {
                     "breadcrumbs": {
@@ -7419,6 +7486,12 @@
                         },
                         "type": "array"
                     },
+                    "childSpaces": {
+                        "items": {
+                            "$ref": "#/components/schemas/Omit_SpaceSummary.userAccess_"
+                        },
+                        "type": "array"
+                    },
                     "parentSpaceUuid": {
                         "type": "string"
                     },
@@ -7475,6 +7548,7 @@
                     }
                 },
                 "required": [
+                    "childSpaces",
                     "slug",
                     "pinnedListOrder",
                     "pinnedListUuid",
@@ -11188,22 +11262,22 @@
                     }
                 ]
             },
-            "PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__": {
+            "PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__": {
                 "properties": {},
                 "type": "object",
                 "description": "Same as `PartialDeep`, but accepts only `object`s as inputs. Internal helper for `PartialDeep`."
             },
-            "PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__": {
+            "PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__": {
                 "properties": {
                     "dashboard": {
-                        "$ref": "#/components/schemas/PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__"
+                        "$ref": "#/components/schemas/PartialObjectDeep___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined__._recurseIntoArrays-true__"
                     }
                 },
                 "type": "object",
                 "description": "Same as `PartialDeep`, but accepts only `object`s as inputs. Internal helper for `PartialDeep`."
             },
             "PartialDeep_DashboardAsCodeLanguageMap._recurseIntoArrays-true__": {
-                "$ref": "#/components/schemas/PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__chartName-string--title-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__",
+                "$ref": "#/components/schemas/PartialObjectDeep__dashboard_58___91_x-string_93__58__name_63_-string-or-undefined--description_63_-string-or-undefined--tiles_63__58__40__type-DashboardTileTypes.SAVED_CHART-or-DashboardTileTypes.SQL_CHART-or-DashboardTileTypes.SEMANTIC_VIEWER_CHART--properties_58__title-string--chartName-string__-or-_type-DashboardTileTypes.MARKDOWN--properties_58__title-string--content-string__-or-_type-DashboardTileTypes.LOOM--properties_58__title-string___41_-Array-or-undefined___._recurseIntoArrays-true__",
                 "description": "Create a type from another type with all keys and nested keys set to optional.\n\nUse-cases:\n- Merging a default settings/config object with another object, the second object would be a deep partial of the default object.\n- Mocking and testing complex entities, where populating an entire object with its keys would be redundant in terms of the mock or test."
             },
             "ApiDashboardAsCodeListResponse": {
diff --git a/packages/backend/src/models/SpaceModel.ts b/packages/backend/src/models/SpaceModel.ts
index 4b589f41b2b01..bacc415a12443 100644
--- a/packages/backend/src/models/SpaceModel.ts
+++ b/packages/backend/src/models/SpaceModel.ts
@@ -345,6 +345,7 @@ export class SpaceModel {
             })),
             projectUuid,
             dashboards: [],
+            childSpaces: [],
             access: [],
             groupsAccess: [],
             slug: space.slug,
@@ -358,6 +359,7 @@ export class SpaceModel {
             spaceUuid?: string;
             spaceUuids?: string[];
             slug?: string;
+            parentSpaceUuid?: string;
         },
         { trx = this.database }: { trx?: Knex } = { trx: this.database },
     ): Promise<Omit<SpaceSummary, 'userAccess'>[]> {
@@ -462,6 +464,12 @@ export class SpaceModel {
                         filters.spaceUuids,
                     );
                 }
+                if (filters.parentSpaceUuid) {
+                    void query.where(
+                        `${SpaceTableName}.parent_space_uuid`,
+                        filters.parentSpaceUuid,
+                    );
+                }
                 if (filters.slug) {
                     void query.where(`${SpaceTableName}.slug`, filters.slug);
                 }
@@ -473,7 +481,10 @@ export class SpaceModel {
     async get(
         spaceUuid: string,
     ): Promise<
-        Omit<Space, 'queries' | 'dashboards' | 'access' | 'groupsAccess'>
+        Omit<
+            Space,
+            'queries' | 'dashboards' | 'access' | 'groupsAccess' | 'childSpaces'
+        >
     > {
         const [row] = await this.database(SpaceTableName)
             .leftJoin(
@@ -1551,6 +1562,9 @@ export class SpaceModel {
             pinnedListOrder: space.pinnedListOrder,
             queries: await this.getSpaceQueries([space.uuid]),
             dashboards: await this.getSpaceDashboards([space.uuid]),
+            childSpaces: await this.find({
+                parentSpaceUuid: spaceUuid,
+            }),
             access:
                 (await this._getSpaceAccess([rootSpaceUuid]))[rootSpaceUuid] ??
                 [],
@@ -1689,6 +1703,7 @@ export class SpaceModel {
             uuid: space.space_uuid,
             projectUuid,
             dashboards: [],
+            childSpaces: [],
             access: [],
             groupsAccess: [],
             pinnedListUuid: null,
diff --git a/packages/backend/src/services/DashboardService/DashboardService.mock.ts b/packages/backend/src/services/DashboardService/DashboardService.mock.ts
index 6bc0ac8b27ee4..bdd768388ceba 100644
--- a/packages/backend/src/services/DashboardService/DashboardService.mock.ts
+++ b/packages/backend/src/services/DashboardService/DashboardService.mock.ts
@@ -66,6 +66,7 @@ export const publicSpace: Space = {
     queries: [],
     projectUuid: 'projectUuid',
     dashboards: [],
+    childSpaces: [],
     access: [],
     groupsAccess: [],
     name: 'public-space',
diff --git a/packages/backend/src/services/ProjectService/ProjectService.mock.ts b/packages/backend/src/services/ProjectService/ProjectService.mock.ts
index 8c7fe09f58a3f..fca2d787b94f5 100644
--- a/packages/backend/src/services/ProjectService/ProjectService.mock.ts
+++ b/packages/backend/src/services/ProjectService/ProjectService.mock.ts
@@ -331,6 +331,7 @@ export const spacesWithSavedCharts: Space[] = [
         ],
         projectUuid,
         dashboards: [],
+        childSpaces: [],
         access: [],
         groupsAccess: [],
     },
@@ -351,6 +352,7 @@ export const spacesWithNoSavedCharts: Space[] = [
         dashboards: [],
         access: [],
         groupsAccess: [],
+        childSpaces: [],
     },
 ];
 
diff --git a/packages/backend/src/services/PromoteService/PromoteService.mock.ts b/packages/backend/src/services/PromoteService/PromoteService.mock.ts
index a4473dbdea978..2e048128a31b7 100644
--- a/packages/backend/src/services/PromoteService/PromoteService.mock.ts
+++ b/packages/backend/src/services/PromoteService/PromoteService.mock.ts
@@ -84,6 +84,7 @@ export const upstreamSpace: UpstreamChart['space'] = {
 
 export const upstreamFullSpace: Space = {
     ...upstreamSpace,
+    childSpaces: [],
     queries: [],
     dashboards: [],
     access: [
diff --git a/packages/common/src/types/space.ts b/packages/common/src/types/space.ts
index 05f26290c8408..0670924e70d9e 100644
--- a/packages/common/src/types/space.ts
+++ b/packages/common/src/types/space.ts
@@ -20,6 +20,7 @@ export type Space = {
     slug: string;
     // Nested Spaces MVP - disables nested spaces' access changes
     parentSpaceUuid?: string;
+    childSpaces: Omit<SpaceSummary, 'userAccess'>[];
     breadcrumbs?: {
         name: string;
         uuid: string;
diff --git a/packages/frontend/src/components/common/MantineModal/index.tsx b/packages/frontend/src/components/common/MantineModal/index.tsx
new file mode 100644
index 0000000000000..1974c7aa66d9f
--- /dev/null
+++ b/packages/frontend/src/components/common/MantineModal/index.tsx
@@ -0,0 +1,127 @@
+import {
+    Flex,
+    type FlexProps,
+    Group,
+    Modal,
+    type ModalBaseSettings,
+    type ModalBodyProps,
+    type ModalContentProps,
+    type ModalHeaderProps,
+    Paper,
+    Stack,
+    Text,
+} from '@mantine/core';
+import { type Icon as IconType } from '@tabler/icons-react';
+import React from 'react';
+import MantineIcon from '../MantineIcon';
+
+const modalSizes = {
+    sm: 380,
+    lg: 480,
+} as const;
+
+type MantineModalProps = {
+    opened: boolean;
+    onClose: () => void;
+    title: string;
+    icon?: IconType;
+    size?: keyof typeof modalSizes;
+    children: React.ReactNode;
+    actions?: React.ReactNode;
+    modalRootProps?: Partial<ModalBaseSettings>;
+    modalContentProps?: Partial<ModalContentProps>;
+    modalHeaderProps?: Partial<ModalHeaderProps>;
+    modalBodyProps?: Partial<ModalBodyProps>;
+    modalActionsProps?: Partial<FlexProps>;
+};
+
+const MantineModal: React.FC<MantineModalProps> = ({
+    opened,
+    onClose,
+    title,
+    icon,
+    size = 'lg',
+    children,
+    actions,
+    modalRootProps,
+    modalContentProps,
+    modalHeaderProps,
+    modalBodyProps,
+    modalActionsProps,
+}) => {
+    return (
+        <Modal.Root
+            opened={opened}
+            onClose={onClose}
+            size={modalSizes[size]}
+            centered
+            {...modalRootProps}
+        >
+            <Modal.Overlay />
+            <Modal.Content
+                sx={{
+                    maxWidth: '800px',
+                    margin: '0 auto',
+                    display: 'flex',
+                    flexDirection: 'column',
+                }}
+                {...modalContentProps}
+            >
+                <Modal.Header
+                    sx={(theme) => ({
+                        borderBottom: `1px solid ${theme.colors.gray[2]}`,
+                    })}
+                    px="xl"
+                    py="md"
+                    {...modalHeaderProps}
+                >
+                    <Group spacing="sm">
+                        {icon ? (
+                            <Paper p="xxs" withBorder radius="sm">
+                                <MantineIcon icon={icon} size="md" />
+                            </Paper>
+                        ) : null}
+                        <Text color="gray.9" fw={700} fz="md">
+                            {title}
+                        </Text>
+                    </Group>
+                    <Modal.CloseButton />
+                </Modal.Header>
+
+                <Modal.Body p={0} {...modalBodyProps}>
+                    <Stack
+                        p="xl"
+                        spacing="md"
+                        sx={() => ({
+                            overflow: 'auto',
+                            maxHeight: 'calc(80vh - 130px)',
+                        })}
+                    >
+                        {children}
+                    </Stack>
+                </Modal.Body>
+                {actions ? (
+                    <Flex
+                        sx={(theme) => ({
+                            borderTop: `1px solid ${theme.colors.gray[2]}`,
+                            backgroundColor: theme.white,
+                            position: 'sticky',
+                            bottom: 0,
+                            width: '100%',
+                            zIndex: 10,
+                        })}
+                        px="xl"
+                        py="md"
+                        justify="flex-end"
+                        gap="sm"
+                        {...modalActionsProps}
+                    >
+                        {actions}
+                    </Flex>
+                ) : null}
+            </Modal.Content>
+        </Modal.Root>
+    );
+};
+
+export default MantineModal;
diff --git a/packages/frontend/src/components/common/SpaceActionModal/DeleteSpaceModalContent.tsx b/packages/frontend/src/components/common/SpaceActionModal/DeleteSpaceModalContent.tsx
index 4a725ef6ebbd5..b5de9980f515c 100644
--- a/packages/frontend/src/components/common/SpaceActionModal/DeleteSpaceModalContent.tsx
+++ b/packages/frontend/src/components/common/SpaceActionModal/DeleteSpaceModalContent.tsx
@@ -1,35 +1,57 @@
+import { Alert, Text } from '@mantine/core';
 import { type FC } from 'react';
 import { type SpaceModalBody } from '.';
 
-const DeleteSpaceModalContent: FC<SpaceModalBody> = ({ data }) => (
-    <>
-        <p>
-            Are you sure you want to delete space <b>"{data?.name}"</b>?
-        </p>
+const DeleteSpaceModalContent: FC<SpaceModalBody> = ({ data }) => {
+    if (
+        !data ||
+        !(
+            data.queries.length > 0 ||
+            data.dashboards.length > 0 ||
+            data.childSpaces.length > 0
+        )
+    ) {
+        return (
+            <p>
+                Are you sure you want to delete space <b>"{data?.name}"</b>?
+            </p>
+        );
+    }
 
-        {data && (data.queries.length > 0 || data.dashboards.length > 0) && (
+    return (
+        <>
             <p>
-                This will delete
-                {data.queries.length > 0 && (
-                    <>
-                        {' '}
-                        {data.queries.length} chart
-                        {data.queries.length === 1 ? '' : 's'}
-                    </>
-                )}
-                {data.queries.length > 0 && data.dashboards.length > 0 && (
-                    <> and</>
-                )}
-                {data.dashboards.length > 0 && (
-                    <>
-                        {' '}
-                        {data.dashboards.length} dashboard
-                        {data.dashboards.length === 1 ? '' : 's'}
-                    </>
-                )}
+                Are you sure you want to delete space <b>"{data?.name}"</b>?
             </p>
-        )}
-    </>
-);
+
+            <Alert color="red">
+                <Text size="sm" color="gray.9">
+                    <strong>This will permanently delete:</strong>
+                </Text>
+                <ul style={{ paddingLeft: '1rem' }}>
+                    {data.queries.length > 0 ? (
+                        <li>
+                            {data.queries.length} chart
+                            {data.queries.length === 1 ? '' : 's'}
+                        </li>
+                    ) : null}
+                    {data.dashboards.length > 0 && (
+                        <li>
+                            {data.dashboards.length} dashboard
+                            {data.dashboards.length === 1 ? '' : 's'}
+                        </li>
+                    )}
+                    {data.childSpaces.length > 0 && (
+                        <li>
+                            {data.childSpaces.length} nested space
+                            {data.childSpaces.length === 1 ? '' : 's'} (and all
+                            their contents)
+                        </li>
+                    )}
+                </ul>
+            </Alert>
+        </>
+    );
+};
 
 export default DeleteSpaceModalContent;
diff --git a/packages/frontend/src/components/common/SpaceActionModal/index.tsx b/packages/frontend/src/components/common/SpaceActionModal/index.tsx
index 9a8bc3b404412..4ad65dfa8634a 100644
--- a/packages/frontend/src/components/common/SpaceActionModal/index.tsx
+++ b/packages/frontend/src/components/common/SpaceActionModal/index.tsx
@@ -6,6 +6,7 @@ import {
 } from '@lightdash/common';
 import {
     Button,
+    Flex,
     Group,
     MantineProvider,
     Modal,
@@ -26,6 +27,7 @@ import {
     useUpdateMutation,
 } from '../../../hooks/useSpaces';
 import MantineIcon from '../MantineIcon';
+import MantineModal from '../MantineModal';
 import { SpacePrivateAccessType } from '../ShareSpaceModal/ShareSpaceSelect';
 import CreateSpaceModalContent from './CreateSpaceModalContent';
 import DeleteSpaceModalContent from './DeleteSpaceModalContent';
@@ -118,6 +120,32 @@ const SpaceModal: FC<ActionModalProps> = ({
         return null;
     }
 
+    if (actionType === ActionType.DELETE) {
+        return (
+            <MantineModal
+                opened
+                onClose={onClose}
+                title={title}
+                icon={icon!}
+                size="sm"
+                actions={
+                    <form name={title} onSubmit={form.onSubmit(handleSubmit)}>
+                        <Flex gap="sm">
+                            <Button variant="default" h={32} onClick={onClose}>
+                                Cancel
+                            </Button>
+                            <Button h={32} type="submit" color="red">
+                                Delete Space
+                            </Button>
+                        </Flex>
+                    </form>
+                }
+            >
+                <DeleteSpaceModalContent data={data} form={form} />
+            </MantineModal>
+        );
+    }
+
     return (
         <MantineProvider inherit theme={{ colorScheme: 'light' }}>
             <Modal
diff --git a/packages/frontend/src/hooks/useSpaces.ts b/packages/frontend/src/hooks/useSpaces.ts
index fae47f6f01844..18517e11d3701 100644
--- a/packages/frontend/src/hooks/useSpaces.ts
+++ b/packages/frontend/src/hooks/useSpaces.ts
@@ -185,7 +185,7 @@ export const useCreateMutation = (
             projectUuid ? createSpace(projectUuid, data) : Promise.reject(),
         {
             mutationKey: ['space_create', projectUuid],
-            onSuccess: async (space) => {
+            onSuccess: async (space, { parentSpaceUuid }) => {
                 await queryClient.invalidateQueries([
                     'projects',
                     projectUuid!,
@@ -194,6 +194,14 @@ export const useCreateMutation = (
 
                 await queryClient.invalidateQueries(['content']);
 
+                if (parentSpaceUuid) {
+                    await queryClient.invalidateQueries([
+                        'space',
+                        projectUuid!,
+                        parentSpaceUuid,
+                    ]);
+                }
+
                 options?.onSuccess?.(space);
 
                 showToastSuccess({


Reviews:
user: login: IrakliJani
body: minor comments but code looks good. testing it in render now
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T10:51:43Z

Review comments:
diff_hunk: @@ -358,6 +359,7 @@ export class SpaceModel {
             spaceUuid?: string;
             spaceUuids?: string[];
             slug?: string;
+            parentSpaceUuid?: string;
path: packages/backend/src/models/SpaceModel.ts
user: login: IrakliJani
body: we may need to change this to `string | null` after the rebase (if my changes go in first)
created_at: 2025-04-17T10:47:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+import {
+    Flex,
+    type FlexProps,
+    Group,
+    Modal,
+    type ModalBaseSettings,
+    type ModalBodyProps,
+    type ModalContentProps,
+    type ModalHeaderProps,
+    Paper,
+    Text,
+} from '@mantine/core';
+import { type Icon as IconType } from '@tabler/icons-react';
+import React from 'react';
+import MantineIcon from '../MantineIcon';
+
+const modalSizes = {
+    sm: 380,
+    lg: 480,
+} as const;
+
+type MantineModalProps = {
+    opened: boolean;
+    onClose: () => void;
+    title: string;
+    icon?: IconType;
+    size?: keyof typeof modalSizes;
+    children: React.ReactNode;
+    actions?: React.ReactNode;
+    modalRootProps?: Partial<ModalBaseSettings>;
+    modalContentProps?: Partial<ModalContentProps>;
+    modalHeaderProps?: Partial<ModalHeaderProps>;
+    modalBodyProps?: Partial<ModalBodyProps>;
+    modalActionsProps?: Partial<FlexProps>;
path: packages/frontend/src/components/common/MantineModal/index.tsx
user: login: IrakliJani
body: sweeeeeet
created_at: 2025-04-17T10:48:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+import {
+    Flex,
+    type FlexProps,
+    Group,
+    Modal,
+    type ModalBaseSettings,
+    type ModalBodyProps,
+    type ModalContentProps,
+    type ModalHeaderProps,
+    Paper,
+    Text,
+} from '@mantine/core';
path: packages/frontend/src/components/common/MantineModal/index.tsx
user: login: IrakliJani
body: we should do this in mantine 7 as an experiment some time soon
created_at: 2025-04-17T10:49:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+import {
+    Flex,
+    type FlexProps,
+    Group,
+    Modal,
+    type ModalBaseSettings,
+    type ModalBodyProps,
+    type ModalContentProps,
+    type ModalHeaderProps,
+    Paper,
+    Text,
+} from '@mantine/core';
+import { type Icon as IconType } from '@tabler/icons-react';
+import React from 'react';
+import MantineIcon from '../MantineIcon';
+
+const modalSizes = {
+    sm: 380,
+    lg: 480,
+} as const;
+
+type MantineModalProps = {
+    opened: boolean;
+    onClose: () => void;
+    title: string;
+    icon?: IconType;
+    size?: keyof typeof modalSizes;
+    children: React.ReactNode;
+    actions?: React.ReactNode;
+    modalRootProps?: Partial<ModalBaseSettings>;
+    modalContentProps?: Partial<ModalContentProps>;
+    modalHeaderProps?: Partial<ModalHeaderProps>;
+    modalBodyProps?: Partial<ModalBodyProps>;
+    modalActionsProps?: Partial<FlexProps>;
+};
+
+const MantineModal: React.FC<MantineModalProps> = ({
+    opened,
+    onClose,
+    title,
+    icon,
+    size = 'lg',
+    children,
+    actions,
+    modalRootProps,
+    modalContentProps,
+    modalHeaderProps,
+    modalBodyProps,
+    modalActionsProps,
+}) => {
+    return (
+        <Modal.Root
+            opened={opened}
+            onClose={onClose}
+            size={modalSizes[size]}
+            centered
+            {...modalRootProps}
+        >
+            <Modal.Overlay />
+            <Modal.Content
+                sx={{
+                    maxWidth: '800px',
+                    margin: '0 auto',
+                    display: 'flex',
+                    flexDirection: 'column',
+                }}
+                {...modalContentProps}
+            >
+                <Modal.Header
+                    sx={(theme) => ({
+                        borderBottom: `1px solid ${theme.colors.gray[2]}`,
+                    })}
+                    px="xl"
+                    py="md"
+                    {...modalHeaderProps}
+                >
+                    <Group spacing="sm">
+                        {icon ? (
+                            <Paper p="xxs" withBorder radius="sm">
+                                <MantineIcon icon={icon} size="md" />
+                            </Paper>
+                        ) : null}
+                        <Text color="gray.9" fw={700} fz="md">
+                            {title}
+                        </Text>
+                    </Group>
+                    <Modal.CloseButton />
+                </Modal.Header>
+
+                <Modal.Body p={0} {...modalBodyProps}>
+                    <Flex
path: packages/frontend/src/components/common/MantineModal/index.tsx
user: login: IrakliJani
body: can be a Stack maybe?
created_at: 2025-04-17T10:50:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -118,6 +120,41 @@ const SpaceModal: FC<ActionModalProps> = ({
         return null;
     }
 
+    if (actionType === ActionType.DELETE) {
+        return (
+            <MantineProvider inherit theme={{ colorScheme: 'light' }}>
path: packages/frontend/src/components/common/SpaceActionModal/index.tsx
user: login: IrakliJani
body: because of the navbar?
created_at: 2025-04-17T10:51:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: notgiorgi
created_at: 2025-04-16T12:55:29Z
body: * **#14453** <a href="https://app.graphite.dev/github/pr/lightdash/lightdash/14453?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#14443** <a href="https://app.graphite.dev/github/pr/lightdash/lightdash/14443?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#14438** <a href="https://app.graphite.dev/github/pr/lightdash/lightdash/14438?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/lightdash/lightdash/14438?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#14428** <a href="https://app.graphite.dev/github/pr/lightdash/lightdash/14428?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: CONTRIBUTOR
user: login: lightdash-bot
created_at: 2025-04-17T12:50:59Z
body: :tada: This PR is included in version 0.1585.0 :tada:

The release is available on:
- `0.1585.0`
- [GitHub release](https://github.com/lightdash/lightdash/releases/tag/0.1585.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR