diff --git a/app/components/ConfirmDeleteModal.tsx b/app/components/ConfirmActionModal.tsx
similarity index 58%
rename from app/components/ConfirmDeleteModal.tsx
rename to app/components/ConfirmActionModal.tsx
index 23670dfaa..f1fb56b90 100644
--- a/app/components/ConfirmDeleteModal.tsx
+++ b/app/components/ConfirmActionModal.tsx
@@ -8,16 +8,13 @@
import { useState } from 'react'
import { type ApiError } from '@oxide/api'
-import { Message, Modal } from '@oxide/ui'
-import { classed } from '@oxide/util'
+import { Modal } from '@oxide/ui'
-import { clearConfirmDelete, useConfirmDelete } from 'app/stores/confirm-delete'
+import { clearConfirmAction, useConfirmAction } from 'app/stores/confirm-action'
import { addToast } from 'app/stores/toast'
-export const HL = classed.span`text-sans-semi-md text-default`
-
-export function ConfirmDeleteModal() {
- const deleteConfig = useConfirmDelete((state) => state.deleteConfig)
+export function ConfirmActionModal() {
+ const actionConfig = useConfirmAction((state) => state.actionConfig)
// this is a bit sad -- ideally we would be able to use the loading state
// from the mutation directly, but that would require a lot of line changes
@@ -25,28 +22,23 @@ export function ConfirmDeleteModal() {
// loading state changes
const [loading, setLoading] = useState(false)
- if (!deleteConfig) return null
-
- const { doDelete, warning, label } = deleteConfig
+ if (!actionConfig) return null
- const displayLabel = typeof label === 'string' ? {label} : label
+ const { doAction, modalContent, errorTitle, modalTitle } = actionConfig
return (
-
-
-
Are you sure you want to delete {displayLabel}?
- {warning && }
-
+
+ {modalContent} {
setLoading(true)
try {
- await doDelete()
+ await doAction()
} catch (error) {
addToast({
variant: 'error',
- title: 'Could not delete resource',
+ title: errorTitle,
content: (error as ApiError).message,
})
}
@@ -54,7 +46,7 @@ export function ConfirmDeleteModal() {
setLoading(false) // do this regardless of success or error
// TODO: generic success toast?
- clearConfirmDelete()
+ clearConfirmAction()
}}
cancelText="Cancel"
actionText="Confirm"
diff --git a/app/main.tsx b/app/main.tsx
index e93384135..4cc71bf5c 100644
--- a/app/main.tsx
+++ b/app/main.tsx
@@ -14,7 +14,7 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { queryClient } from '@oxide/api'
import { SkipLink } from '@oxide/ui'
-import { ConfirmDeleteModal } from './components/ConfirmDeleteModal'
+import { ConfirmActionModal } from './components/ConfirmActionModal'
import { ErrorBoundary } from './components/ErrorBoundary'
import { ReduceMotion } from './hooks'
// stripped out by rollup in production
@@ -45,7 +45,7 @@ function render() {
-
+
diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx
index 5ae95a1d3..7e6c83081 100644
--- a/app/pages/SiloAccessPage.tsx
+++ b/app/pages/SiloAccessPage.tsx
@@ -32,13 +32,12 @@ import {
import { groupBy, isTruthy } from '@oxide/util'
import { AccessNameCell } from 'app/components/AccessNameCell'
-import { HL } from 'app/components/ConfirmDeleteModal'
import { RoleBadgeCell } from 'app/components/RoleBadgeCell'
import {
SiloAccessAddUserSideModal,
SiloAccessEditUserSideModal,
} from 'app/forms/silo-access'
-import { confirmDelete } from 'app/stores/confirm-delete'
+import { confirmDelete, HL } from 'app/stores/confirm-delete'
const EmptyState = ({ onClick }: { onClick: () => void }) => (
diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx
index 07a9d9929..1026a5bdd 100644
--- a/app/pages/project/access/ProjectAccessPage.tsx
+++ b/app/pages/project/access/ProjectAccessPage.tsx
@@ -35,14 +35,13 @@ import {
import { groupBy, isTruthy } from '@oxide/util'
import { AccessNameCell } from 'app/components/AccessNameCell'
-import { HL } from 'app/components/ConfirmDeleteModal'
import { RoleBadgeCell } from 'app/components/RoleBadgeCell'
import {
ProjectAccessAddUserSideModal,
ProjectAccessEditUserSideModal,
} from 'app/forms/project-access'
import { getProjectSelector, useProjectSelector } from 'app/hooks'
-import { confirmDelete } from 'app/stores/confirm-delete'
+import { confirmDelete, HL } from 'app/stores/confirm-delete'
const EmptyState = ({ onClick }: { onClick: () => void }) => (
diff --git a/app/pages/project/instances/actions.tsx b/app/pages/project/instances/actions.tsx
index c2ee1d3ab..a98eb4324 100644
--- a/app/pages/project/instances/actions.tsx
+++ b/app/pages/project/instances/actions.tsx
@@ -111,12 +111,6 @@ export const useMakeInstanceActions = (
options.onDelete?.()
addToast({ title: `Deleting instance '${instance.name}'` })
},
- onError: (error) =>
- addToast({
- variant: 'error',
- title: `Error deleting instance '${instance.name}'`,
- content: error.message,
- }),
}),
label: instance.name,
}),
diff --git a/app/stores/confirm-delete.ts b/app/stores/confirm-action.ts
similarity index 57%
rename from app/stores/confirm-delete.ts
rename to app/stores/confirm-action.ts
index 06d0c3cad..642a2d87c 100644
--- a/app/stores/confirm-delete.ts
+++ b/app/stores/confirm-action.ts
@@ -8,25 +8,22 @@
import type { ReactNode } from 'react'
import { create } from 'zustand'
-type DeleteConfig = {
+type ActionConfig = {
/** Must be `mutateAsync`, otherwise we can't catch the error generically */
- doDelete: () => Promise
- warning?: string
- /**
- * Label identifying the resource. Could be a name or something more elaborate
- * "the Admin role for user Harry Styles". If a string, the modal will
- * automatically give it a highlighted style. Otherwise it will be rendered
- * directly.
- */
- label: ReactNode
+ doAction: () => Promise
+ /** e.g., Confirm delete, Confirm unlink */
+ modalTitle: string
+ modalContent: ReactNode
+ /** Title of error toast */
+ errorTitle: string
}
-type ConfirmDeleteStore = {
- deleteConfig: DeleteConfig | null
+type ConfirmActionStore = {
+ actionConfig: ActionConfig | null
}
-export const useConfirmDelete = create(() => ({
- deleteConfig: null,
+export const useConfirmAction = create(() => ({
+ actionConfig: null,
}))
// zustand docs say this pattern is equivalent to putting the actions on the
@@ -39,10 +36,10 @@ export const useConfirmDelete = create(() => ({
/**
* Note that this returns a function so we can save a line in the calling code.
*/
-export const confirmDelete = (deleteConfig: DeleteConfig) => () => {
- useConfirmDelete.setState({ deleteConfig })
+export const confirmAction = (actionConfig: ActionConfig) => () => {
+ useConfirmAction.setState({ actionConfig })
}
-export function clearConfirmDelete() {
- useConfirmDelete.setState({ deleteConfig: null })
+export function clearConfirmAction() {
+ useConfirmAction.setState({ actionConfig: null })
}
diff --git a/app/stores/confirm-delete.tsx b/app/stores/confirm-delete.tsx
new file mode 100644
index 000000000..fd2327157
--- /dev/null
+++ b/app/stores/confirm-delete.tsx
@@ -0,0 +1,42 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * Copyright Oxide Computer Company
+ */
+
+import { classed } from '@oxide/util'
+
+import { useConfirmAction } from './confirm-action'
+
+// confirmAction was originally abstracted from confirmDelete. this preserves
+// the existing confirmDelete API by constructing a confirmAction from it
+
+type DeleteConfig = {
+ /** Must be `mutateAsync`, otherwise we can't catch the error generically */
+ doDelete: () => Promise
+ /**
+ * Label identifying the resource. Could be a name or something more elaborate
+ * "the Admin role for user Harry Styles". If a string, the modal will
+ * automatically give it a highlighted style. Otherwise it will be rendered
+ * directly.
+ */
+ label: React.ReactNode
+}
+
+export const HL = classed.span`text-sans-semi-md text-default`
+
+export const confirmDelete =
+ ({ doDelete, label }: DeleteConfig) =>
+ () => {
+ const displayLabel = typeof label === 'string' ? {label} : label
+ useConfirmAction.setState({
+ actionConfig: {
+ doAction: doDelete,
+ modalContent: