mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 15:46:38 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebf412b9df | |||
| 9a85363e44 |
@@ -0,0 +1,262 @@
|
||||
# Announcement Popup Notification Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a popup modal for announcements that automatically shows to users when a new or updated announcement is published.
|
||||
|
||||
**Architecture:** We will modify the Go backend to return `update_time` along with the announcement data. In the Vite frontend, we will store the user's `flvx_announcement_seen_time` in `localStorage`. If the fetched `update_time` is greater than the stored timestamp, we trigger a NextUI Modal displaying the announcement content.
|
||||
|
||||
**Tech Stack:** Go, Vite, React, TailwindCSS, NextUI.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update API Response in Go Backend
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/internal/http/handler/handler.go`
|
||||
|
||||
- [ ] **Step 1: Write the minimal implementation**
|
||||
|
||||
Modify the `getAnnouncement` function in `go-backend/internal/http/handler/handler.go`.
|
||||
Find the response map inside `getAnnouncement` and add the `update_time` key:
|
||||
|
||||
```go
|
||||
if ann == nil {
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"content": "",
|
||||
"enabled": 0,
|
||||
"update_time": 0,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
updateTime := ann.CreatedTime
|
||||
if ann.UpdatedTime.Valid {
|
||||
updateTime = ann.UpdatedTime.Int64
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"content": ann.Content,
|
||||
"enabled": ann.Enabled,
|
||||
"update_time": updateTime,
|
||||
}))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/http/handler/handler.go
|
||||
git commit -m "feat(api): include update_time in announcement response"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update Frontend API Interface
|
||||
|
||||
**Files:**
|
||||
- Modify: `vite-frontend/src/api/index.ts`
|
||||
|
||||
- [ ] **Step 1: Write the minimal implementation**
|
||||
|
||||
Modify the `AnnouncementData` interface in `vite-frontend/src/api/index.ts` to include `update_time`.
|
||||
|
||||
```typescript
|
||||
export interface AnnouncementData {
|
||||
content: string;
|
||||
enabled: number;
|
||||
update_time?: number;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add vite-frontend/src/api/index.ts
|
||||
git commit -m "feat(ui): add update_time to AnnouncementData interface"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create AnnouncementModal Component
|
||||
|
||||
**Files:**
|
||||
- Create: `vite-frontend/src/pages/dashboard/components/announcement-modal.tsx`
|
||||
|
||||
- [ ] **Step 1: Write the minimal implementation**
|
||||
|
||||
Create `vite-frontend/src/pages/dashboard/components/announcement-modal.tsx` with the following content:
|
||||
|
||||
```tsx
|
||||
import type { AnnouncementData } from "@/api";
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@/shadcn-bridge/heroui/modal";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface AnnouncementModalProps {
|
||||
announcement: AnnouncementData;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onDontShowAgain: () => void;
|
||||
}
|
||||
|
||||
export const AnnouncementModal = ({
|
||||
announcement,
|
||||
isOpen,
|
||||
onClose,
|
||||
onDontShowAgain,
|
||||
}: AnnouncementModalProps) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(open) => !open && onClose()} size="2xl">
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">平台公告</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none max-h-[60vh] overflow-y-auto">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{announcement.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="flat" onPress={onDontShowAgain}>
|
||||
不再提示
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add vite-frontend/src/pages/dashboard/components/announcement-modal.tsx
|
||||
git commit -m "feat(ui): create AnnouncementModal component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Integrate Modal State in Dashboard Custom Hook
|
||||
|
||||
**Files:**
|
||||
- Modify: `vite-frontend/src/pages/dashboard/use-dashboard-data.ts`
|
||||
|
||||
- [ ] **Step 1: Update the hook return type interface**
|
||||
|
||||
At the top of `vite-frontend/src/pages/dashboard/use-dashboard-data.ts` where `DashboardData` is or similar, add the new properties (if it uses an explicit return type). If it's inferred, skip this. Wait, let's check the code:
|
||||
|
||||
```typescript
|
||||
isAnnouncementModalOpen: boolean;
|
||||
setIsAnnouncementModalOpen: (isOpen: boolean) => void;
|
||||
dismissAnnouncementModal: () => void;
|
||||
```
|
||||
Ensure they are added to the returned object at the bottom of the `useDashboardData` hook.
|
||||
|
||||
Find the `const loadAnnouncement` function.
|
||||
|
||||
- [ ] **Step 2: Write the minimal implementation**
|
||||
|
||||
First, add state at the top of the hook:
|
||||
```typescript
|
||||
const [isAnnouncementModalOpen, setIsAnnouncementModalOpen] = useState(false);
|
||||
```
|
||||
|
||||
Then, modify the `loadAnnouncement` logic inside `useDashboardData`:
|
||||
```typescript
|
||||
if (res.code === 0 && res.data && res.data.enabled === 1) {
|
||||
setAnnouncement(res.data);
|
||||
|
||||
try {
|
||||
const storedTimeStr = localStorage.getItem("flvx_announcement_seen_time");
|
||||
const storedTime = storedTimeStr ? parseInt(storedTimeStr, 10) : 0;
|
||||
const updateTime = res.data.update_time || 0;
|
||||
|
||||
if (updateTime > storedTime) {
|
||||
setIsAnnouncementModalOpen(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Failed to read localStorage for announcement state", err);
|
||||
setIsAnnouncementModalOpen(true);
|
||||
}
|
||||
} else {
|
||||
setAnnouncement(null);
|
||||
}
|
||||
```
|
||||
|
||||
Add the dismiss handler inside the hook:
|
||||
```typescript
|
||||
const dismissAnnouncementModal = useCallback(() => {
|
||||
setIsAnnouncementModalOpen(false);
|
||||
if (announcement && announcement.update_time) {
|
||||
try {
|
||||
localStorage.setItem("flvx_announcement_seen_time", announcement.update_time.toString());
|
||||
} catch (err) {
|
||||
console.warn("Failed to set localStorage for announcement state", err);
|
||||
}
|
||||
}
|
||||
}, [announcement]);
|
||||
```
|
||||
|
||||
Ensure these are included in the return object of the hook:
|
||||
```typescript
|
||||
isAnnouncementModalOpen,
|
||||
setIsAnnouncementModalOpen,
|
||||
dismissAnnouncementModal,
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add vite-frontend/src/pages/dashboard/use-dashboard-data.ts
|
||||
git commit -m "feat(ui): manage announcement modal state in dashboard hook"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add Modal to Dashboard Layout
|
||||
|
||||
**Files:**
|
||||
- Modify: `vite-frontend/src/pages/dashboard.tsx`
|
||||
|
||||
- [ ] **Step 1: Write the minimal implementation**
|
||||
|
||||
Import the modal component at the top:
|
||||
```tsx
|
||||
import { AnnouncementModal } from "@/pages/dashboard/components/announcement-modal";
|
||||
```
|
||||
|
||||
Add the new properties to the destructured `useDashboardData` object:
|
||||
```tsx
|
||||
isAnnouncementModalOpen,
|
||||
setIsAnnouncementModalOpen,
|
||||
dismissAnnouncementModal,
|
||||
```
|
||||
|
||||
Add the modal instance near the end of the dashboard rendering (just below `{announcement && <AnnouncementBanner ... />}` or inside the main `<div>`):
|
||||
```tsx
|
||||
{announcement && (
|
||||
<AnnouncementModal
|
||||
announcement={announcement}
|
||||
isOpen={isAnnouncementModalOpen}
|
||||
onClose={() => setIsAnnouncementModalOpen(false)}
|
||||
onDontShowAgain={dismissAnnouncementModal}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add vite-frontend/src/pages/dashboard.tsx
|
||||
git commit -m "feat(ui): add announcement modal to dashboard layout"
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# Announcement Popup Notification Design
|
||||
|
||||
## Overview
|
||||
This feature implements a popup notification modal for important dashboard announcements to ensure users see them immediately, addressing GitHub Issue #169.
|
||||
|
||||
## Requirements
|
||||
1. Automatic display of a popup modal when opening the dashboard page if a new/updated announcement exists.
|
||||
2. Includes a "Don't show again" option to remember the user's choice to dismiss it.
|
||||
3. Smart triggering: Only pops up for *new* or *updated* announcements.
|
||||
4. Support Markdown formatting for the announcement content.
|
||||
5. Retain the existing permanent top banner as a fallback.
|
||||
|
||||
## Backend Changes (Go)
|
||||
The `/api/v1/announcement/get` API currently only returns `content` and `enabled`. It must be updated to return the timestamp of the last update to enable the frontend to detect changes.
|
||||
|
||||
1. **Repository (`internal/store/repo/repository.go`)**: Ensure `GetAnnouncement` retrieves `UpdatedTime` (or falls back to `CreatedTime`).
|
||||
2. **Handler (`internal/http/handler/handler.go`)**: Modify `getAnnouncement` to include an `update_time` (int64) field in its JSON response.
|
||||
|
||||
## Frontend Changes (Vite/React/Tailwind)
|
||||
1. **API Interface (`src/api/index.ts`)**:
|
||||
* Update `AnnouncementData` to include `update_time: number`.
|
||||
2. **Storage Mechanism**:
|
||||
* Use browser `localStorage` to persist the user's view state. Key: `flvx_announcement_seen_time`.
|
||||
3. **UI Component (`AnnouncementModal`)**:
|
||||
* Create a new modal component for the dashboard.
|
||||
* The modal content will render the markdown of the announcement.
|
||||
* It will feature two primary actions:
|
||||
* **"Close"**: Closes the modal temporarily for this session (does NOT update `localStorage`). It will pop up again on the next page load.
|
||||
* **"Don't show again"**: Closes the modal AND sets `localStorage.setItem('flvx_announcement_seen_time', announcement.update_time)`.
|
||||
4. **Integration (`src/pages/dashboard.tsx` & `use-dashboard-data.ts`)**:
|
||||
* Add state to manage the modal visibility (e.g., `isAnnouncementModalOpen`).
|
||||
* On data load, compare the fetched `update_time` with the stored `flvx_announcement_seen_time`. If the fetched time is greater (or if no stored time exists), set `isAnnouncementModalOpen(true)`.
|
||||
|
||||
## Error Handling and Edge Cases
|
||||
* If `localStorage` is unavailable or throws an error (e.g., Private Browsing mode restrictions), the modal may show repeatedly. The code should safely catch `localStorage` access errors.
|
||||
* If `update_time` is missing from an old database record, the backend should gracefully fall back to the creation time or a safe default (like 0) to ensure the logic doesn't break.
|
||||
@@ -1537,15 +1537,22 @@ func (h *Handler) getAnnouncement(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if ann == nil {
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"content": "",
|
||||
"enabled": 0,
|
||||
"content": "",
|
||||
"enabled": 0,
|
||||
"update_time": 0,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
updateTime := ann.CreatedTime
|
||||
if ann.UpdatedTime.Valid {
|
||||
updateTime = ann.UpdatedTime.Int64
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"content": ann.Content,
|
||||
"enabled": ann.Enabled,
|
||||
"content": ann.Content,
|
||||
"enabled": ann.Enabled,
|
||||
"update_time": updateTime,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -410,6 +410,7 @@ export const importBackup = (data: BackupImportPayload) =>
|
||||
export interface AnnouncementData {
|
||||
content: string;
|
||||
enabled: number;
|
||||
update_time?: number;
|
||||
}
|
||||
|
||||
export const getAnnouncement = () =>
|
||||
|
||||
@@ -662,7 +662,7 @@ export default function AdminLayout({
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={() => {
|
||||
onOpenChange();
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@/shadcn-bridge/heroui/modal";
|
||||
import { PageEmptyState, PageLoadingState } from "@/components/page-state";
|
||||
import { AnnouncementBanner } from "@/pages/dashboard/components/announcement-banner";
|
||||
import { AnnouncementModal } from "@/pages/dashboard/components/announcement-modal";
|
||||
import { FlowChartCard } from "@/pages/dashboard/components/flow-chart-card";
|
||||
import { MetricCard } from "@/pages/dashboard/components/metric-card";
|
||||
import {
|
||||
@@ -43,6 +44,9 @@ export default function DashboardPage() {
|
||||
nodeExpiryReminders,
|
||||
isAdmin,
|
||||
announcement,
|
||||
isAnnouncementModalOpen,
|
||||
setIsAnnouncementModalOpen,
|
||||
dismissAnnouncementModal,
|
||||
} = useDashboardData();
|
||||
|
||||
const [addressModalOpen, setAddressModalOpen] = useState(false);
|
||||
@@ -627,6 +631,14 @@ export default function DashboardPage() {
|
||||
return (
|
||||
<AnimatedPage className="px-3 lg:px-6 py-2 lg:py-4">
|
||||
{announcement && <AnnouncementBanner announcement={announcement} />}
|
||||
{announcement && (
|
||||
<AnnouncementModal
|
||||
announcement={announcement}
|
||||
isOpen={isAnnouncementModalOpen}
|
||||
onClose={() => setIsAnnouncementModalOpen(false)}
|
||||
onDontShowAgain={dismissAnnouncementModal}
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4 mb-6 lg:mb-8">
|
||||
<MetricCard
|
||||
icon={
|
||||
@@ -1091,7 +1103,7 @@ export default function DashboardPage() {
|
||||
backdrop="blur"
|
||||
isOpen={addressModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={() => setAddressModalOpen(false)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { AnnouncementData } from "@/api";
|
||||
import { Button } from "@/shadcn-bridge/heroui/button";
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@/shadcn-bridge/heroui/modal";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface AnnouncementModalProps {
|
||||
announcement: AnnouncementData;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onDontShowAgain: () => void;
|
||||
}
|
||||
|
||||
export const AnnouncementModal = ({
|
||||
announcement,
|
||||
isOpen,
|
||||
onClose,
|
||||
onDontShowAgain,
|
||||
}: AnnouncementModalProps) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={(open) => !open && onClose()} size="2xl">
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">平台公告</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none max-h-[60vh] overflow-y-auto">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{announcement.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="flat" onPress={onDontShowAgain}>
|
||||
不再提示
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -96,6 +96,9 @@ interface DashboardDataState {
|
||||
nodeExpiryReminders: DashboardNodeExpiryItem[];
|
||||
isAdmin: boolean;
|
||||
announcement: AnnouncementData | null;
|
||||
isAnnouncementModalOpen: boolean;
|
||||
setIsAnnouncementModalOpen: (isOpen: boolean) => void;
|
||||
dismissAnnouncementModal: () => void;
|
||||
}
|
||||
|
||||
const checkExpirationNotifications = (
|
||||
@@ -267,6 +270,7 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
const [announcement, setAnnouncement] = useState<AnnouncementData | null>(
|
||||
null,
|
||||
);
|
||||
const [isAnnouncementModalOpen, setIsAnnouncementModalOpen] = useState(false);
|
||||
const isMountedRef = useRef(true);
|
||||
const packageRequestInFlightRef = useRef(false);
|
||||
const nodeExpiryRequestInFlightRef = useRef(false);
|
||||
@@ -345,6 +349,19 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
|
||||
if (res.code === 0 && res.data && res.data.enabled === 1) {
|
||||
setAnnouncement(res.data);
|
||||
|
||||
try {
|
||||
const storedTimeStr = localStorage.getItem("flvx_announcement_seen_time");
|
||||
const storedTime = storedTimeStr ? parseInt(storedTimeStr, 10) : 0;
|
||||
const updateTime = res.data.update_time || 0;
|
||||
|
||||
if (updateTime > storedTime) {
|
||||
setIsAnnouncementModalOpen(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Failed to read localStorage for announcement state", err);
|
||||
setIsAnnouncementModalOpen(true);
|
||||
}
|
||||
} else {
|
||||
setAnnouncement(null);
|
||||
}
|
||||
@@ -355,6 +372,17 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dismissAnnouncementModal = useCallback(() => {
|
||||
setIsAnnouncementModalOpen(false);
|
||||
if (announcement && announcement.update_time) {
|
||||
try {
|
||||
localStorage.setItem("flvx_announcement_seen_time", announcement.update_time.toString());
|
||||
} catch (err) {
|
||||
console.warn("Failed to set localStorage for announcement state", err);
|
||||
}
|
||||
}
|
||||
}, [announcement]);
|
||||
|
||||
const loadNodeExpiryData = useCallback(async () => {
|
||||
if (nodeExpiryRequestInFlightRef.current) {
|
||||
return;
|
||||
@@ -438,5 +466,8 @@ export const useDashboardData = (): DashboardDataState => {
|
||||
nodeExpiryReminders,
|
||||
isAdmin,
|
||||
announcement,
|
||||
isAnnouncementModalOpen,
|
||||
setIsAnnouncementModalOpen,
|
||||
dismissAnnouncementModal,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4702,7 +4702,7 @@ export default function ForwardPage() {
|
||||
}}
|
||||
isOpen={modalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={setModalOpen}
|
||||
>
|
||||
@@ -4917,7 +4917,7 @@ export default function ForwardPage() {
|
||||
}}
|
||||
isOpen={deleteModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
>
|
||||
@@ -4962,7 +4962,7 @@ export default function ForwardPage() {
|
||||
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
|
||||
}}
|
||||
isOpen={addressModalOpen}
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="lg"
|
||||
onClose={() => setAddressModalOpen(false)}
|
||||
>
|
||||
@@ -5007,7 +5007,7 @@ export default function ForwardPage() {
|
||||
}}
|
||||
isOpen={exportModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={() => {
|
||||
setExportModalOpen(false);
|
||||
@@ -5166,7 +5166,7 @@ export default function ForwardPage() {
|
||||
}}
|
||||
isOpen={importModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={() => setImportModalOpen(false)}
|
||||
>
|
||||
|
||||
@@ -432,7 +432,7 @@ export default function LimitPage() {
|
||||
}}
|
||||
isOpen={modalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={setModalOpen}
|
||||
>
|
||||
@@ -510,7 +510,7 @@ export default function LimitPage() {
|
||||
}}
|
||||
isOpen={deleteModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
>
|
||||
|
||||
@@ -2410,7 +2410,7 @@ export default function NodePage() {
|
||||
}}
|
||||
isOpen={dialogVisible}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={() => setDialogVisible(false)}
|
||||
>
|
||||
@@ -2831,7 +2831,7 @@ export default function NodePage() {
|
||||
}}
|
||||
isOpen={rollbackModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={setRollbackModalOpen}
|
||||
>
|
||||
@@ -2872,7 +2872,7 @@ export default function NodePage() {
|
||||
}}
|
||||
isOpen={deleteModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
>
|
||||
@@ -2968,7 +2968,7 @@ export default function NodePage() {
|
||||
}}
|
||||
isOpen={installCommandModal}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={() => setInstallCommandModal(false)}
|
||||
>
|
||||
@@ -3025,7 +3025,7 @@ export default function NodePage() {
|
||||
}}
|
||||
isOpen={upgradeModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="md"
|
||||
onOpenChange={setUpgradeModalOpen}
|
||||
>
|
||||
@@ -3131,7 +3131,7 @@ export default function NodePage() {
|
||||
}}
|
||||
isOpen={batchDeleteModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="md"
|
||||
onOpenChange={setBatchDeleteModalOpen}
|
||||
>
|
||||
|
||||
@@ -332,7 +332,7 @@ export default function ProfilePage() {
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={() => {
|
||||
onOpenChange();
|
||||
|
||||
@@ -2005,7 +2005,7 @@ export default function TunnelPage() {
|
||||
}}
|
||||
isOpen={modalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={setModalOpen}
|
||||
>
|
||||
@@ -2942,7 +2942,7 @@ export default function TunnelPage() {
|
||||
}}
|
||||
isOpen={deleteModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onOpenChange={handleDeleteModalOpenChange}
|
||||
>
|
||||
|
||||
@@ -1455,7 +1455,7 @@ export default function UserPage() {
|
||||
}}
|
||||
isOpen={isUserModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={onUserModalClose}
|
||||
>
|
||||
@@ -1698,7 +1698,7 @@ export default function UserPage() {
|
||||
isDismissable={false}
|
||||
isOpen={isTunnelModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={onTunnelModalClose}
|
||||
>
|
||||
@@ -2035,7 +2035,7 @@ export default function UserPage() {
|
||||
isDismissable={false}
|
||||
isOpen={isEditTunnelModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={onEditTunnelModalClose}
|
||||
>
|
||||
@@ -2215,7 +2215,7 @@ export default function UserPage() {
|
||||
}}
|
||||
isOpen={isDeleteModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={onDeleteModalClose}
|
||||
>
|
||||
@@ -2261,7 +2261,7 @@ export default function UserPage() {
|
||||
}}
|
||||
isOpen={isDeleteTunnelModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={onDeleteTunnelModalClose}
|
||||
>
|
||||
@@ -2309,7 +2309,7 @@ export default function UserPage() {
|
||||
}}
|
||||
isOpen={isResetFlowModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={onResetFlowModalClose}
|
||||
>
|
||||
@@ -2401,7 +2401,7 @@ export default function UserPage() {
|
||||
}}
|
||||
isOpen={isResetTunnelFlowModalOpen}
|
||||
placement="center"
|
||||
scrollBehavior="outside"
|
||||
scrollBehavior="inside"
|
||||
size="2xl"
|
||||
onClose={onResetTunnelFlowModalClose}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user