feat: maintenance flows frontend foundation - types, API clients, sidebar, library filter
- Add maintenance.ts types: TargetEntry, TargetList, MaintenanceSchedule, BatchLaunch - Add targetListsApi and maintenanceSchedulesApi/batchLaunchApi clients - Extend TreeType union with 'maintenance' in tree.ts - Update getTreeNavigatePath/getTreeEditorPath in routing.ts for maintenance - Sidebar: track maintenance count and add Maintenance sub-nav item - TreeLibraryPage: add maintenance to typeFilter state, URL sync, and tab buttons - TreeGridView, TreeListView, TreeTableView: add amber Wrench maintenance badge Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,3 +13,5 @@ export { default as adminApi } from './admin'
|
|||||||
export { treeMarkdownApi } from './treeMarkdown'
|
export { treeMarkdownApi } from './treeMarkdown'
|
||||||
export { default as pinnedFlowsApi } from './pinnedFlows'
|
export { default as pinnedFlowsApi } from './pinnedFlows'
|
||||||
export { default as analyticsApi } from './analytics'
|
export { default as analyticsApi } from './analytics'
|
||||||
|
export { targetListsApi } from './targetLists'
|
||||||
|
export { maintenanceSchedulesApi, batchLaunchApi } from './maintenanceSchedules'
|
||||||
|
|||||||
24
frontend/src/api/maintenanceSchedules.ts
Normal file
24
frontend/src/api/maintenanceSchedules.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { apiClient } from './client'
|
||||||
|
import type {
|
||||||
|
MaintenanceSchedule,
|
||||||
|
MaintenanceScheduleCreate,
|
||||||
|
MaintenanceScheduleUpdate,
|
||||||
|
BatchLaunchRequest,
|
||||||
|
BatchLaunchResponse,
|
||||||
|
} from '@/types'
|
||||||
|
|
||||||
|
export const maintenanceSchedulesApi = {
|
||||||
|
getForTree: (treeId: string): Promise<MaintenanceSchedule> =>
|
||||||
|
apiClient.get(`/maintenance-schedules/tree/${treeId}`).then(r => r.data),
|
||||||
|
|
||||||
|
create: (data: MaintenanceScheduleCreate): Promise<MaintenanceSchedule> =>
|
||||||
|
apiClient.post('/maintenance-schedules', data).then(r => r.data),
|
||||||
|
|
||||||
|
update: (id: string, data: MaintenanceScheduleUpdate): Promise<MaintenanceSchedule> =>
|
||||||
|
apiClient.patch(`/maintenance-schedules/${id}`, data).then(r => r.data),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const batchLaunchApi = {
|
||||||
|
launch: (data: BatchLaunchRequest): Promise<BatchLaunchResponse> =>
|
||||||
|
apiClient.post('/sessions/batch', data).then(r => r.data),
|
||||||
|
}
|
||||||
19
frontend/src/api/targetLists.ts
Normal file
19
frontend/src/api/targetLists.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { apiClient } from './client'
|
||||||
|
import type { TargetList, TargetListCreate } from '@/types'
|
||||||
|
|
||||||
|
export const targetListsApi = {
|
||||||
|
list: (): Promise<TargetList[]> =>
|
||||||
|
apiClient.get('/target-lists/').then(r => r.data),
|
||||||
|
|
||||||
|
get: (id: string): Promise<TargetList> =>
|
||||||
|
apiClient.get(`/target-lists/${id}`).then(r => r.data),
|
||||||
|
|
||||||
|
create: (data: TargetListCreate): Promise<TargetList> =>
|
||||||
|
apiClient.post('/target-lists/', data).then(r => r.data),
|
||||||
|
|
||||||
|
update: (id: string, data: Partial<TargetListCreate>): Promise<TargetList> =>
|
||||||
|
apiClient.put(`/target-lists/${id}`, data).then(r => r.data),
|
||||||
|
|
||||||
|
delete: (id: string): Promise<void> =>
|
||||||
|
apiClient.delete(`/target-lists/${id}`).then(() => undefined),
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ export function Sidebar() {
|
|||||||
const [activeTags, setActiveTags] = useState<string[]>([])
|
const [activeTags, setActiveTags] = useState<string[]>([])
|
||||||
const [activeSessionCount, setActiveSessionCount] = useState(0)
|
const [activeSessionCount, setActiveSessionCount] = useState(0)
|
||||||
const [pinnedFlows, setPinnedFlows] = useState<PinnedFlow[]>([])
|
const [pinnedFlows, setPinnedFlows] = useState<PinnedFlow[]>([])
|
||||||
const [treeCounts, setTreeCounts] = useState({ total: 0, troubleshooting: 0, procedural: 0 })
|
const [treeCounts, setTreeCounts] = useState({ total: 0, troubleshooting: 0, procedural: 0, maintenance: 0 })
|
||||||
|
|
||||||
// Fetch sidebar data on mount
|
// Fetch sidebar data on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,7 +55,8 @@ export function Sidebar() {
|
|||||||
const total = allTrees.length
|
const total = allTrees.length
|
||||||
const troubleshooting = allTrees.filter(t => t.tree_type === 'troubleshooting').length
|
const troubleshooting = allTrees.filter(t => t.tree_type === 'troubleshooting').length
|
||||||
const procedural = allTrees.filter(t => t.tree_type === 'procedural').length
|
const procedural = allTrees.filter(t => t.tree_type === 'procedural').length
|
||||||
setTreeCounts({ total, troubleshooting, procedural })
|
const maintenance = allTrees.filter(t => t.tree_type === 'maintenance').length
|
||||||
|
setTreeCounts({ total, troubleshooting, procedural, maintenance })
|
||||||
} catch {
|
} catch {
|
||||||
// Silently handle errors
|
// Silently handle errors
|
||||||
}
|
}
|
||||||
@@ -145,6 +146,7 @@ export function Sidebar() {
|
|||||||
children={[
|
children={[
|
||||||
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: treeCounts.troubleshooting || undefined },
|
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: treeCounts.troubleshooting || undefined },
|
||||||
{ href: '/trees?type=procedural', label: 'Projects', count: treeCounts.procedural || undefined },
|
{ href: '/trees?type=procedural', label: 'Projects', count: treeCounts.procedural || undefined },
|
||||||
|
{ href: '/trees?type=maintenance', label: 'Maintenance', count: treeCounts.maintenance || undefined },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<NavItem href="/my-trees" icon={PenLine} label="Flow Editor" />
|
<NavItem href="/my-trees" icon={PenLine} label="Flow Editor" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText } from 'lucide-react'
|
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench } from 'lucide-react'
|
||||||
import type { TreeListItem } from '@/types'
|
import type { TreeListItem } from '@/types'
|
||||||
import { TagBadges } from '@/components/common/TagBadges'
|
import { TagBadges } from '@/components/common/TagBadges'
|
||||||
import { AddToFolderMenu } from './AddToFolderMenu'
|
import { AddToFolderMenu } from './AddToFolderMenu'
|
||||||
@@ -41,6 +41,12 @@ export function TreeGridView({
|
|||||||
Draft
|
Draft
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{tree.tree_type === 'maintenance' && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wide text-amber-400">
|
||||||
|
<Wrench className="h-3 w-3" />
|
||||||
|
Maintenance
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{tree.is_public ? (
|
{tree.is_public ? (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Pencil, Globe, Lock, GitBranch, FileText, Trash2 } from 'lucide-react'
|
import { Pencil, Globe, Lock, GitBranch, FileText, Trash2, Wrench } from 'lucide-react'
|
||||||
import type { TreeListItem } from '@/types'
|
import type { TreeListItem } from '@/types'
|
||||||
import { TagBadges } from '@/components/common/TagBadges'
|
import { TagBadges } from '@/components/common/TagBadges'
|
||||||
import { AddToFolderMenu } from './AddToFolderMenu'
|
import { AddToFolderMenu } from './AddToFolderMenu'
|
||||||
@@ -42,6 +42,12 @@ export function TreeListView({
|
|||||||
Draft
|
Draft
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{tree.tree_type === 'maintenance' && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wide text-amber-400 flex-shrink-0">
|
||||||
|
<Wrench className="h-3 w-3" />
|
||||||
|
Maintenance
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{tree.is_public ? (
|
{tree.is_public ? (
|
||||||
<span title="Public tree">
|
<span title="Public tree">
|
||||||
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2 } from 'lucide-react'
|
import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2, Wrench } from 'lucide-react'
|
||||||
import type { TreeListItem } from '@/types'
|
import type { TreeListItem } from '@/types'
|
||||||
import { TagBadges } from '@/components/common/TagBadges'
|
import { TagBadges } from '@/components/common/TagBadges'
|
||||||
import { AddToFolderMenu } from './AddToFolderMenu'
|
import { AddToFolderMenu } from './AddToFolderMenu'
|
||||||
@@ -144,6 +144,12 @@ export function TreeTableView({
|
|||||||
Draft
|
Draft
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{tree.tree_type === 'maintenance' && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wide text-amber-400 flex-shrink-0">
|
||||||
|
<Wrench className="h-3 w-3" />
|
||||||
|
Maintenance
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{tree.is_public ? (
|
{tree.is_public ? (
|
||||||
<span title="Public tree">
|
<span title="Public tree">
|
||||||
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
<Globe className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Shared routing helpers for tree/session navigation.
|
* Shared routing helpers for tree/session navigation.
|
||||||
* Centralizes the logic for determining the correct navigation path
|
* Centralizes the logic for determining the correct navigation path
|
||||||
* based on tree type (troubleshooting vs procedural).
|
* based on tree type (troubleshooting vs procedural vs maintenance).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,6 +14,9 @@ export function getTreeNavigatePath(
|
|||||||
if (treeType === 'procedural') {
|
if (treeType === 'procedural') {
|
||||||
return `/flows/${treeId}/navigate`
|
return `/flows/${treeId}/navigate`
|
||||||
}
|
}
|
||||||
|
if (treeType === 'maintenance') {
|
||||||
|
return `/flows/${treeId}/maintenance`
|
||||||
|
}
|
||||||
return `/trees/${treeId}/navigate`
|
return `/trees/${treeId}/navigate`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +27,7 @@ export function getTreeEditorPath(
|
|||||||
treeId: string,
|
treeId: string,
|
||||||
treeType?: string
|
treeType?: string
|
||||||
): string {
|
): string {
|
||||||
if (treeType === 'procedural') {
|
if (treeType === 'procedural' || treeType === 'maintenance') {
|
||||||
return `/flows/${treeId}/edit`
|
return `/flows/${treeId}/edit`
|
||||||
}
|
}
|
||||||
return `/trees/${treeId}/edit`
|
return `/trees/${treeId}/edit`
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ export function TreeLibraryPage() {
|
|||||||
|
|
||||||
// Read type filter from URL query params (e.g. /trees?type=procedural)
|
// Read type filter from URL query params (e.g. /trees?type=procedural)
|
||||||
const urlType = searchParams.get('type')
|
const urlType = searchParams.get('type')
|
||||||
const [typeFilter, setTypeFilter] = useState<'all' | 'troubleshooting' | 'procedural'>(
|
const [typeFilter, setTypeFilter] = useState<'all' | 'troubleshooting' | 'procedural' | 'maintenance'>(
|
||||||
urlType === 'troubleshooting' || urlType === 'procedural' ? urlType : 'all'
|
urlType === 'troubleshooting' || urlType === 'procedural' || urlType === 'maintenance' ? urlType : 'all'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sync filters when URL changes (e.g. clicking sidebar categories/tags or nav sub-items)
|
// Sync filters when URL changes (e.g. clicking sidebar categories/tags or nav sub-items)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = searchParams.get('type')
|
const t = searchParams.get('type')
|
||||||
if (t === 'troubleshooting' || t === 'procedural') {
|
if (t === 'troubleshooting' || t === 'procedural' || t === 'maintenance') {
|
||||||
setTypeFilter(t)
|
setTypeFilter(t)
|
||||||
} else {
|
} else {
|
||||||
setTypeFilter('all')
|
setTypeFilter('all')
|
||||||
@@ -253,14 +253,16 @@ export function TreeLibraryPage() {
|
|||||||
<div className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">
|
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">
|
||||||
{typeFilter === 'procedural' ? 'Projects' : typeFilter === 'troubleshooting' ? 'Troubleshooting Flows' : 'Flow Library'}
|
{typeFilter === 'procedural' ? 'Projects' : typeFilter === 'troubleshooting' ? 'Troubleshooting Flows' : typeFilter === 'maintenance' ? 'Maintenance Flows' : 'Flow Library'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
{typeFilter === 'procedural'
|
{typeFilter === 'procedural'
|
||||||
? 'Step-by-step projects and runbooks'
|
? 'Step-by-step projects and runbooks'
|
||||||
: typeFilter === 'troubleshooting'
|
: typeFilter === 'troubleshooting'
|
||||||
? 'Branching decision flows for troubleshooting'
|
? 'Branching decision flows for troubleshooting'
|
||||||
: 'Browse and start troubleshooting flows and projects'}
|
: typeFilter === 'maintenance'
|
||||||
|
? 'Scheduled maintenance procedures run across targets'
|
||||||
|
: 'Browse and start troubleshooting flows and projects'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{canCreateTrees && (
|
{canCreateTrees && (
|
||||||
@@ -326,7 +328,7 @@ export function TreeLibraryPage() {
|
|||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex rounded-lg border border-border p-0.5">
|
<div className="flex rounded-lg border border-border p-0.5">
|
||||||
{(['all', 'troubleshooting', 'procedural'] as const).map((t) => (
|
{(['all', 'troubleshooting', 'procedural', 'maintenance'] as const).map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t}
|
key={t}
|
||||||
onClick={() => setTypeFilter(t)}
|
onClick={() => setTypeFilter(t)}
|
||||||
@@ -337,7 +339,7 @@ export function TreeLibraryPage() {
|
|||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{t === 'all' ? 'All' : t === 'troubleshooting' ? 'Troubleshooting' : 'Projects'}
|
{t === 'all' ? 'All' : t === 'troubleshooting' ? 'Troubleshooting' : t === 'procedural' ? 'Projects' : 'Maintenance'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,3 +23,14 @@ export interface PaginatedResponse<T> {
|
|||||||
export interface ApiError {
|
export interface ApiError {
|
||||||
detail: string
|
detail: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
TargetEntry,
|
||||||
|
TargetList,
|
||||||
|
TargetListCreate,
|
||||||
|
MaintenanceSchedule,
|
||||||
|
MaintenanceScheduleCreate,
|
||||||
|
MaintenanceScheduleUpdate,
|
||||||
|
BatchLaunchRequest,
|
||||||
|
BatchLaunchResponse,
|
||||||
|
} from './maintenance'
|
||||||
|
|||||||
65
frontend/src/types/maintenance.ts
Normal file
65
frontend/src/types/maintenance.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
export interface TargetEntry {
|
||||||
|
label: string
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TargetList {
|
||||||
|
id: string
|
||||||
|
team_id: string
|
||||||
|
created_by?: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
targets: TargetEntry[]
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TargetListCreate {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
targets: TargetEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaintenanceSchedule {
|
||||||
|
id: string
|
||||||
|
tree_id: string
|
||||||
|
created_by?: string
|
||||||
|
cron_expression: string
|
||||||
|
timezone: string
|
||||||
|
target_list_id?: string
|
||||||
|
is_active: boolean
|
||||||
|
next_run_at?: string
|
||||||
|
last_run_at?: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaintenanceScheduleCreate {
|
||||||
|
tree_id: string
|
||||||
|
cron_expression: string
|
||||||
|
timezone: string
|
||||||
|
target_list_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaintenanceScheduleUpdate {
|
||||||
|
cron_expression?: string
|
||||||
|
timezone?: string
|
||||||
|
target_list_id?: string
|
||||||
|
is_active?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchLaunchRequest {
|
||||||
|
tree_id: string
|
||||||
|
targets: TargetEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchLaunchResponse {
|
||||||
|
batch_id: string
|
||||||
|
count: number
|
||||||
|
sessions: Array<{
|
||||||
|
id: string
|
||||||
|
batch_id: string
|
||||||
|
target_label: string
|
||||||
|
tree_id: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ export interface TreeStructure {
|
|||||||
|
|
||||||
// --- Procedural Flow Types ---
|
// --- Procedural Flow Types ---
|
||||||
|
|
||||||
export type TreeType = 'troubleshooting' | 'procedural'
|
export type TreeType = 'troubleshooting' | 'procedural' | 'maintenance'
|
||||||
|
|
||||||
export type IntakeFieldType =
|
export type IntakeFieldType =
|
||||||
| 'text' | 'textarea' | 'number' | 'ip_address' | 'email' | 'url'
|
| 'text' | 'textarea' | 'number' | 'ip_address' | 'email' | 'url'
|
||||||
|
|||||||
Reference in New Issue
Block a user