mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
add archive device instead of delete
This commit is contained in:
@@ -2394,5 +2394,17 @@
|
|||||||
"maintenanceScreenTitle": "Service Temporarily Unavailable",
|
"maintenanceScreenTitle": "Service Temporarily Unavailable",
|
||||||
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
|
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
|
||||||
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
|
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
|
||||||
"createInternalResourceDialogDestinationRequired": "Destination is required"
|
"createInternalResourceDialogDestinationRequired": "Destination is required",
|
||||||
|
"available": "Available",
|
||||||
|
"archived": "Archived",
|
||||||
|
"noArchivedDevices": "No archived devices found",
|
||||||
|
"deviceArchived": "Device archived",
|
||||||
|
"deviceArchivedDescription": "The device has been successfully archived.",
|
||||||
|
"errorArchivingDevice": "Error archiving device",
|
||||||
|
"failedToArchiveDevice": "Failed to archive device",
|
||||||
|
"deviceQuestionArchive": "Are you sure you want to archive this device?",
|
||||||
|
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
|
||||||
|
"deviceArchiveConfirm": "Archive Device",
|
||||||
|
"archiveDevice": "Archive Device",
|
||||||
|
"archive": "Archive"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -726,7 +726,8 @@ export const olms = pgTable("olms", {
|
|||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}),
|
||||||
|
archived: boolean("archived").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const olmSessions = pgTable("clientSession", {
|
export const olmSessions = pgTable("clientSession", {
|
||||||
|
|||||||
@@ -423,7 +423,8 @@ export const olms = sqliteTable("olms", {
|
|||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}),
|
||||||
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
|
||||||
|
|||||||
@@ -808,11 +808,11 @@ authenticated.put("/user/:userId/olm", verifyIsLoggedInUser, olm.createUserOlm);
|
|||||||
|
|
||||||
authenticated.get("/user/:userId/olms", verifyIsLoggedInUser, olm.listUserOlms);
|
authenticated.get("/user/:userId/olms", verifyIsLoggedInUser, olm.listUserOlms);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.post(
|
||||||
"/user/:userId/olm/:olmId",
|
"/user/:userId/olm/:olmId/archive",
|
||||||
verifyIsLoggedInUser,
|
verifyIsLoggedInUser,
|
||||||
verifyOlmAccess,
|
verifyOlmAccess,
|
||||||
olm.deleteUserOlm
|
olm.archiveUserOlm
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
|
|||||||
93
server/routers/olm/archiveUserOlm.ts
Normal file
93
server/routers/olm/archiveUserOlm.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { olms, clients } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||||
|
import { sendTerminateClient } from "../client/terminate";
|
||||||
|
|
||||||
|
const paramsSchema = z
|
||||||
|
.object({
|
||||||
|
userId: z.string(),
|
||||||
|
olmId: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
// registry.registerPath({
|
||||||
|
// method: "post",
|
||||||
|
// path: "/user/{userId}/olm/{olmId}/archive",
|
||||||
|
// description: "Archive an olm for a user.",
|
||||||
|
// tags: [OpenAPITags.User, OpenAPITags.Client],
|
||||||
|
// request: {
|
||||||
|
// params: paramsSchema
|
||||||
|
// },
|
||||||
|
// responses: {}
|
||||||
|
// });
|
||||||
|
|
||||||
|
export async function archiveUserOlm(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { olmId } = parsedParams.data;
|
||||||
|
|
||||||
|
// Archive the OLM and disconnect associated clients in a transaction
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// Find all clients associated with this OLM
|
||||||
|
const associatedClients = await trx
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.olmId, olmId));
|
||||||
|
|
||||||
|
// Disconnect clients from the OLM (set olmId to null)
|
||||||
|
for (const client of associatedClients) {
|
||||||
|
await trx
|
||||||
|
.update(clients)
|
||||||
|
.set({ olmId: null })
|
||||||
|
.where(eq(clients.clientId, client.clientId));
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromClient(client, trx);
|
||||||
|
await sendTerminateClient(client.clientId, olmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive the OLM (set archived to true)
|
||||||
|
await trx
|
||||||
|
.update(olms)
|
||||||
|
.set({ archived: true })
|
||||||
|
.where(eq(olms.olmId, olmId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Device archived successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to archive device"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,8 @@ export * from "./getOlmToken";
|
|||||||
export * from "./createUserOlm";
|
export * from "./createUserOlm";
|
||||||
export * from "./handleOlmRelayMessage";
|
export * from "./handleOlmRelayMessage";
|
||||||
export * from "./handleOlmPingMessage";
|
export * from "./handleOlmPingMessage";
|
||||||
export * from "./deleteUserOlm";
|
export * from "./archiveUserOlm";
|
||||||
export * from "./listUserOlms";
|
export * from "./listUserOlms";
|
||||||
export * from "./deleteUserOlm";
|
|
||||||
export * from "./getUserOlm";
|
export * from "./getUserOlm";
|
||||||
export * from "./handleOlmServerPeerAddMessage";
|
export * from "./handleOlmServerPeerAddMessage";
|
||||||
export * from "./handleOlmUnRelayMessage";
|
export * from "./handleOlmUnRelayMessage";
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type ListUserOlmsResponse = {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
clientId: number | null;
|
clientId: number | null;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
|
archived: boolean;
|
||||||
}>;
|
}>;
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -89,7 +90,7 @@ export async function listUserOlms(
|
|||||||
|
|
||||||
const { userId } = parsedParams.data;
|
const { userId } = parsedParams.data;
|
||||||
|
|
||||||
// Get total count
|
// Get total count (including archived OLMs)
|
||||||
const [totalCountResult] = await db
|
const [totalCountResult] = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(olms)
|
.from(olms)
|
||||||
@@ -97,7 +98,7 @@ export async function listUserOlms(
|
|||||||
|
|
||||||
const total = totalCountResult?.count || 0;
|
const total = totalCountResult?.count || 0;
|
||||||
|
|
||||||
// Get OLMs for the current user
|
// Get OLMs for the current user (including archived OLMs)
|
||||||
const userOlms = await db
|
const userOlms = await db
|
||||||
.select({
|
.select({
|
||||||
olmId: olms.olmId,
|
olmId: olms.olmId,
|
||||||
@@ -105,7 +106,8 @@ export async function listUserOlms(
|
|||||||
version: olms.version,
|
version: olms.version,
|
||||||
name: olms.name,
|
name: olms.name,
|
||||||
clientId: olms.clientId,
|
clientId: olms.clientId,
|
||||||
userId: olms.userId
|
userId: olms.userId,
|
||||||
|
archived: olms.archived
|
||||||
})
|
})
|
||||||
.from(olms)
|
.from(olms)
|
||||||
.where(eq(olms.userId, userId))
|
.where(eq(olms.userId, userId))
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow
|
TableRow
|
||||||
} from "@app/components/ui/table";
|
} from "@app/components/ui/table";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@app/components/ui/tabs";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { Loader2, RefreshCw } from "lucide-react";
|
import { Loader2, RefreshCw } from "lucide-react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
@@ -44,6 +45,7 @@ type Device = {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
clientId: number | null;
|
clientId: number | null;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
|
archived: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ViewDevicesDialog({
|
export default function ViewDevicesDialog({
|
||||||
@@ -57,8 +59,9 @@ export default function ViewDevicesDialog({
|
|||||||
|
|
||||||
const [devices, setDevices] = useState<Device[]>([]);
|
const [devices, setDevices] = useState<Device[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isArchiveModalOpen, setIsArchiveModalOpen] = useState(false);
|
||||||
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
|
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState<"available" | "archived">("available");
|
||||||
|
|
||||||
const fetchDevices = async () => {
|
const fetchDevices = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -90,26 +93,31 @@ export default function ViewDevicesDialog({
|
|||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const deleteDevice = async (olmId: string) => {
|
const archiveDevice = async (olmId: string) => {
|
||||||
try {
|
try {
|
||||||
await api.delete(`/user/${user?.userId}/olm/${olmId}`);
|
await api.post(`/user/${user?.userId}/olm/${olmId}/archive`);
|
||||||
toast({
|
toast({
|
||||||
title: t("deviceDeleted") || "Device deleted",
|
title: t("deviceArchived") || "Device archived",
|
||||||
description:
|
description:
|
||||||
t("deviceDeletedDescription") ||
|
t("deviceArchivedDescription") ||
|
||||||
"The device has been successfully deleted."
|
"The device has been successfully archived."
|
||||||
});
|
});
|
||||||
setDevices(devices.filter((d) => d.olmId !== olmId));
|
// Update the device's archived status in the local state
|
||||||
setIsDeleteModalOpen(false);
|
setDevices(
|
||||||
|
devices.map((d) =>
|
||||||
|
d.olmId === olmId ? { ...d, archived: true } : d
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setIsArchiveModalOpen(false);
|
||||||
setSelectedDevice(null);
|
setSelectedDevice(null);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Error deleting device:", error);
|
console.error("Error archiving device:", error);
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("errorDeletingDevice") || "Error deleting device",
|
title: t("errorArchivingDevice"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
error,
|
error,
|
||||||
t("failedToDeleteDevice") || "Failed to delete device"
|
t("failedToArchiveDevice")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -118,7 +126,7 @@ export default function ViewDevicesDialog({
|
|||||||
function reset() {
|
function reset() {
|
||||||
setDevices([]);
|
setDevices([]);
|
||||||
setSelectedDevice(null);
|
setSelectedDevice(null);
|
||||||
setIsDeleteModalOpen(false);
|
setIsArchiveModalOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -147,61 +155,159 @@ export default function ViewDevicesDialog({
|
|||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : devices.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
{t("noDevices") || "No devices found"}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-md border">
|
<Tabs
|
||||||
<Table>
|
value={activeTab}
|
||||||
<TableHeader>
|
onValueChange={(value) =>
|
||||||
<TableRow>
|
setActiveTab(value as "available" | "archived")
|
||||||
<TableHead className="pl-3">
|
}
|
||||||
{t("name") || "Name"}
|
className="w-full"
|
||||||
</TableHead>
|
>
|
||||||
<TableHead>
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
{t("dateCreated") ||
|
<TabsTrigger value="available">
|
||||||
"Date Created"}
|
{t("available") || "Available"} (
|
||||||
</TableHead>
|
{
|
||||||
<TableHead>
|
devices.filter(
|
||||||
{t("actions") || "Actions"}
|
(d) => !d.archived
|
||||||
</TableHead>
|
).length
|
||||||
</TableRow>
|
}
|
||||||
</TableHeader>
|
)
|
||||||
<TableBody>
|
</TabsTrigger>
|
||||||
{devices.map((device) => (
|
<TabsTrigger value="archived">
|
||||||
<TableRow key={device.olmId}>
|
{t("archived") || "Archived"} (
|
||||||
<TableCell className="font-medium">
|
{
|
||||||
{device.name ||
|
devices.filter(
|
||||||
t("unnamedDevice") ||
|
(d) => d.archived
|
||||||
"Unnamed Device"}
|
).length
|
||||||
</TableCell>
|
}
|
||||||
<TableCell>
|
)
|
||||||
{moment(
|
</TabsTrigger>
|
||||||
device.dateCreated
|
</TabsList>
|
||||||
).format("lll")}
|
<TabsContent value="available" className="mt-4">
|
||||||
</TableCell>
|
{devices.filter((d) => !d.archived)
|
||||||
<TableCell>
|
.length === 0 ? (
|
||||||
<Button
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
variant="outline"
|
{t("noDevices") ||
|
||||||
onClick={() => {
|
"No devices found"}
|
||||||
setSelectedDevice(
|
</div>
|
||||||
device
|
) : (
|
||||||
);
|
<div className="rounded-md border">
|
||||||
setIsDeleteModalOpen(
|
<Table>
|
||||||
true
|
<TableHeader>
|
||||||
);
|
<TableRow>
|
||||||
}}
|
<TableHead className="pl-3">
|
||||||
>
|
{t("name") || "Name"}
|
||||||
{t("delete") ||
|
</TableHead>
|
||||||
"Delete"}
|
<TableHead>
|
||||||
</Button>
|
{t("dateCreated") ||
|
||||||
</TableCell>
|
"Date Created"}
|
||||||
</TableRow>
|
</TableHead>
|
||||||
))}
|
<TableHead>
|
||||||
</TableBody>
|
{t("actions") ||
|
||||||
</Table>
|
"Actions"}
|
||||||
</div>
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{devices
|
||||||
|
.filter(
|
||||||
|
(d) => !d.archived
|
||||||
|
)
|
||||||
|
.map((device) => (
|
||||||
|
<TableRow
|
||||||
|
key={device.olmId}
|
||||||
|
>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{device.name ||
|
||||||
|
t(
|
||||||
|
"unnamedDevice"
|
||||||
|
) ||
|
||||||
|
"Unnamed Device"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{moment(
|
||||||
|
device.dateCreated
|
||||||
|
).format(
|
||||||
|
"lll"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDevice(
|
||||||
|
device
|
||||||
|
);
|
||||||
|
setIsArchiveModalOpen(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"archive"
|
||||||
|
) ||
|
||||||
|
"Archive"}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="archived" className="mt-4">
|
||||||
|
{devices.filter((d) => d.archived)
|
||||||
|
.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
{t("noArchivedDevices") ||
|
||||||
|
"No archived devices found"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="pl-3">
|
||||||
|
{t("name") || "Name"}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
{t("dateCreated") ||
|
||||||
|
"Date Created"}
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{devices
|
||||||
|
.filter(
|
||||||
|
(d) => d.archived
|
||||||
|
)
|
||||||
|
.map((device) => (
|
||||||
|
<TableRow
|
||||||
|
key={device.olmId}
|
||||||
|
>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{device.name ||
|
||||||
|
t(
|
||||||
|
"unnamedDevice"
|
||||||
|
) ||
|
||||||
|
"Unnamed Device"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{moment(
|
||||||
|
device.dateCreated
|
||||||
|
).format(
|
||||||
|
"lll"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
@@ -216,9 +322,9 @@ export default function ViewDevicesDialog({
|
|||||||
|
|
||||||
{selectedDevice && (
|
{selectedDevice && (
|
||||||
<ConfirmDeleteDialog
|
<ConfirmDeleteDialog
|
||||||
open={isDeleteModalOpen}
|
open={isArchiveModalOpen}
|
||||||
setOpen={(val) => {
|
setOpen={(val) => {
|
||||||
setIsDeleteModalOpen(val);
|
setIsArchiveModalOpen(val);
|
||||||
if (!val) {
|
if (!val) {
|
||||||
setSelectedDevice(null);
|
setSelectedDevice(null);
|
||||||
}
|
}
|
||||||
@@ -226,19 +332,19 @@ export default function ViewDevicesDialog({
|
|||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p>
|
<p>
|
||||||
{t("deviceQuestionRemove") ||
|
{t("deviceQuestionArchive") ||
|
||||||
"Are you sure you want to delete this device?"}
|
"Are you sure you want to archive this device?"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{t("deviceMessageRemove") ||
|
{t("deviceMessageArchive") ||
|
||||||
"This action cannot be undone."}
|
"The device will be archived and removed from your active devices list."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t("deviceDeleteConfirm") || "Delete Device"}
|
buttonText={t("deviceArchiveConfirm") || "Archive Device"}
|
||||||
onConfirm={async () => deleteDevice(selectedDevice.olmId)}
|
onConfirm={async () => archiveDevice(selectedDevice.olmId)}
|
||||||
string={selectedDevice.name || selectedDevice.olmId}
|
string={selectedDevice.name || selectedDevice.olmId}
|
||||||
title={t("deleteDevice") || "Delete Device"}
|
title={t("archiveDevice") || "Archive Device"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user