reap: cascade orgs → companies → teams → agents
Selecting a team/company/org in the Agents sidebar used to only
delete the grouping row; the agents inside survived, ungrouped.
Not what the user wanted, and it left a trail of orphaned runtime
state (containers, .brain files, DB rows) behind.
Backend — POST /api/claws/batch-delete is now a universal cascade
reaper. Body accepts { ids?, teams?, companies?, orgs? } in any
combination. The server walks org → companies_of_org →
teams_of_company → agents_of_team, dedupes against explicit ids,
and hard-purges every unique agent (deprovision ZeroClaw runtime,
tear down sandbox container, unlink .brain/.onion files,
transactional agents::hard_purge). Group rows are deleted last; FK
cascades on team_members, company_teams, org_companies, and
loop_agents/teams/orgs clean up the join tables. Every stage
streams SSE.
Three new cm-db helpers wire the walk: agents_of_team,
teams_of_company, companies_of_org — all DISTINCT selects on the
existing join tables.
Frontend — Dashboard's Agents-tier StructureTree now sets
selectLevel="*" (was "claw"), so the Wrench → checkbox affordance
appears on org/company/team/agent nodes alike; the same-level
invariant in onToggleSelect still prevents mixed batches.
ReapProgressModal collapses to a single POST regardless of kind —
body key derived from kind — and its subtitle is honest:
"Cascading through every agent inside — permanent."
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT DISTINCT team_id FROM company_teams WHERE company_id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "team_id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "640d23c926f2354e6b22fc7a406635e1019efaf74b0a0e95a0c92c7997b9c005"
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT DISTINCT company_id FROM org_companies WHERE org_id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "company_id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "938e0bc9ff77600bf956618c78192dcca85cf2580bb496606178a59d7939bdcc"
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT DISTINCT claw_id FROM team_members WHERE team_id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "claw_id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c2cdec949bd2aa1500a79a3c6018b0a3afb8230b407c64be54c89932050d9683"
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use cm_domain::{AccessPolicy, Agent, AgentId, AgentStatus};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::runtime_provision::provider_alias_for;
|
use crate::runtime_provision::provider_alias_for;
|
||||||
use crate::{ApiError, AppState, Authed};
|
use crate::{ApiError, AppState, Authed};
|
||||||
@@ -981,26 +982,74 @@ pub async fn delete(
|
|||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct BatchDeleteRequest {
|
pub struct BatchDeleteRequest {
|
||||||
|
#[serde(default)]
|
||||||
pub ids: Vec<AgentId>,
|
pub ids: Vec<AgentId>,
|
||||||
|
/// Cascade: teams/companies/orgs are expanded to the agents inside them,
|
||||||
|
/// every unique agent is hard-purged, then the group rows themselves are
|
||||||
|
/// deleted. FK cascades already remove the join tables; we still delete
|
||||||
|
/// the entity rows explicitly so `list_*` immediately reflects the reap.
|
||||||
|
#[serde(default)]
|
||||||
|
pub teams: Vec<Uuid>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub companies: Vec<Uuid>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub orgs: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `POST /api/claws/batch-delete` (SSE) — HARD-purge multiple agents and reap
|
/// `POST /api/claws/batch-delete` (SSE) — HARD-purge every selected agent,
|
||||||
/// every attached resource: deprovision the ZeroClaw runtime, tear down the
|
/// team, company, or org. For groups, the backend walks the tree (org →
|
||||||
/// sandbox container, unlink the `.brain`/`.onion` files, then transactionally
|
/// companies → teams → agents) and reaps every unique agent underneath:
|
||||||
/// purge all DB rows (`agents::hard_purge`). Streams per-agent/per-resource
|
/// deprovision the ZeroClaw runtime, tear down the sandbox container,
|
||||||
/// progress; the agents vanish from the roster (hard delete) on refresh.
|
/// unlink `.brain`/`.onion` files, then transactionally purge DB rows via
|
||||||
|
/// `agents::hard_purge`. After all agents are gone, the group rows
|
||||||
|
/// themselves are deleted (children FK-cascade). Streams per-stage progress.
|
||||||
pub async fn batch_delete(
|
pub async fn batch_delete(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Authed(user): Authed,
|
Authed(user): Authed,
|
||||||
Json(body): Json<BatchDeleteRequest>,
|
Json(body): Json<BatchDeleteRequest>,
|
||||||
) -> impl axum::response::IntoResponse {
|
) -> impl axum::response::IntoResponse {
|
||||||
let stream = async_stream::stream! {
|
let stream = async_stream::stream! {
|
||||||
let total = body.ids.len().max(1);
|
// Expand groups → collect a de-duplicated agent list. The group ids
|
||||||
|
// are retained so we can delete the entity rows after the reap.
|
||||||
|
use std::collections::HashSet;
|
||||||
|
let mut agent_ids: Vec<AgentId> = Vec::new();
|
||||||
|
let mut seen: HashSet<uuid::Uuid> = HashSet::new();
|
||||||
|
for a in &body.ids {
|
||||||
|
if seen.insert(a.as_uuid()) { agent_ids.push(*a); }
|
||||||
|
}
|
||||||
|
// Orgs → companies → teams → agents
|
||||||
|
let mut team_ids: HashSet<uuid::Uuid> = body.teams.iter().copied().collect();
|
||||||
|
let mut company_ids: HashSet<uuid::Uuid> = body.companies.iter().copied().collect();
|
||||||
|
for org_id in &body.orgs {
|
||||||
|
match cm_db::repo::orgs::companies_of_org(&state.pool, *org_id).await {
|
||||||
|
Ok(cs) => for c in cs { company_ids.insert(c); },
|
||||||
|
Err(e) => { yield sse(json!({"stage":"error","pct":0,"label":format!("org {org_id} expand failed: {e}")})); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for company_id in &company_ids.clone() {
|
||||||
|
match cm_db::repo::companies::teams_of_company(&state.pool, *company_id).await {
|
||||||
|
Ok(ts) => for t in ts { team_ids.insert(t); },
|
||||||
|
Err(e) => { yield sse(json!({"stage":"error","pct":0,"label":format!("company {company_id} expand failed: {e}")})); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for team_id in &team_ids {
|
||||||
|
match cm_db::repo::teams::agents_of_team(&state.pool, *team_id).await {
|
||||||
|
Ok(ags) => for a in ags {
|
||||||
|
if seen.insert(a) { agent_ids.push(AgentId::from(a)); }
|
||||||
|
},
|
||||||
|
Err(e) => { yield sse(json!({"stage":"error","pct":0,"label":format!("team {team_id} expand failed: {e}")})); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let group_count = team_ids.len() + company_ids.len() + body.orgs.len();
|
||||||
|
if group_count > 0 {
|
||||||
|
yield sse(json!({"stage":"start","pct":0,"label":format!("Reaping {} agents from {} groups…", agent_ids.len(), group_count)}));
|
||||||
|
}
|
||||||
|
let total = (agent_ids.len() + group_count).max(1);
|
||||||
let provisioner = crate::runtime_provision::RuntimeProvisioner::from_env();
|
let provisioner = crate::runtime_provision::RuntimeProvisioner::from_env();
|
||||||
let mut done = 0usize;
|
let mut done = 0usize;
|
||||||
for id in body.ids {
|
for id in agent_ids {
|
||||||
let base = 100 * done / total;
|
let base = 100 * done / total;
|
||||||
let agent = match workspace_agent(&state, &user, id).await {
|
let agent = match workspace_agent(&state, &user, id).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
@@ -1039,6 +1088,27 @@ pub async fn batch_delete(
|
|||||||
Err(e) => { done += 1; yield sse(json!({"stage":"error","pct":100 * done / total,"label":format!("{name}: purge failed: {e}")})); }
|
Err(e) => { done += 1; yield sse(json!({"stage":"error","pct":100 * done / total,"label":format!("{name}: purge failed: {e}")})); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Now that every descendant agent is gone, remove the group rows
|
||||||
|
// themselves. FK cascades on team_members / company_teams /
|
||||||
|
// org_companies clean up the join tables automatically.
|
||||||
|
for team_id in &team_ids {
|
||||||
|
match cm_db::repo::teams::delete_team(&state.pool, *team_id, user.workspace_id).await {
|
||||||
|
Ok(()) => { done += 1; yield sse(json!({"stage":"removed","pct":100 * done / total,"label":format!("✓ team {team_id} removed")})); }
|
||||||
|
Err(e) => { done += 1; yield sse(json!({"stage":"error","pct":100 * done / total,"label":format!("team {team_id} delete failed: {e}")})); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for company_id in &company_ids {
|
||||||
|
match cm_db::repo::companies::delete_company(&state.pool, *company_id, user.workspace_id).await {
|
||||||
|
Ok(()) => { done += 1; yield sse(json!({"stage":"removed","pct":100 * done / total,"label":format!("✓ company {company_id} removed")})); }
|
||||||
|
Err(e) => { done += 1; yield sse(json!({"stage":"error","pct":100 * done / total,"label":format!("company {company_id} delete failed: {e}")})); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for org_id in &body.orgs {
|
||||||
|
match cm_db::repo::orgs::delete_org(&state.pool, *org_id, user.workspace_id).await {
|
||||||
|
Ok(()) => { done += 1; yield sse(json!({"stage":"removed","pct":100 * done / total,"label":format!("✓ org {org_id} removed")})); }
|
||||||
|
Err(e) => { done += 1; yield sse(json!({"stage":"error","pct":100 * done / total,"label":format!("org {org_id} delete failed: {e}")})); }
|
||||||
|
}
|
||||||
|
}
|
||||||
yield sse(json!({"stage":"done","pct":100,"label":"Done"}));
|
yield sse(json!({"stage":"done","pct":100,"label":"Done"}));
|
||||||
};
|
};
|
||||||
Sse::new(Box::pin(stream)).keep_alive(KeepAlive::new())
|
Sse::new(Box::pin(stream)).keep_alive(KeepAlive::new())
|
||||||
|
|||||||
@@ -194,3 +194,15 @@ pub async fn teams_for_company(
|
|||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Distinct team ids bound to a company via `company_teams`. The
|
||||||
|
/// cascade-reap path drills through here to collect agents.
|
||||||
|
pub async fn teams_of_company(pool: &PgPool, company_id: Uuid) -> Result<Vec<Uuid>, DbError> {
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
"SELECT DISTINCT team_id FROM company_teams WHERE company_id = $1",
|
||||||
|
company_id,
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(rows.into_iter().map(|r| r.team_id).collect())
|
||||||
|
}
|
||||||
|
|||||||
@@ -164,3 +164,15 @@ pub async fn delete_org(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> R
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Distinct company ids bound to an org via `org_companies`. The
|
||||||
|
/// cascade-reap path drills through here to collect teams → agents.
|
||||||
|
pub async fn companies_of_org(pool: &PgPool, org_id: Uuid) -> Result<Vec<Uuid>, DbError> {
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
"SELECT DISTINCT company_id FROM org_companies WHERE org_id = $1",
|
||||||
|
org_id,
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(rows.into_iter().map(|r| r.company_id).collect())
|
||||||
|
}
|
||||||
|
|||||||
@@ -190,6 +190,18 @@ pub async fn get_team(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Res
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Distinct agent ids bound to a team via `team_members`. Used by the
|
||||||
|
/// cascade-reap path so deleting a team also purges the agents inside it.
|
||||||
|
pub async fn agents_of_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<Uuid>, DbError> {
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
"SELECT DISTINCT claw_id FROM team_members WHERE team_id = $1",
|
||||||
|
team_id,
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(rows.into_iter().map(|r| r.claw_id).collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// The node→claw bindings for a team.
|
/// The node→claw bindings for a team.
|
||||||
pub async fn members_for_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<TeamMember>, DbError> {
|
pub async fn members_for_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<TeamMember>, DbError> {
|
||||||
let rows = sqlx::query!(
|
let rows = sqlx::query!(
|
||||||
|
|||||||
@@ -656,7 +656,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
|
{/* The collapsible org → company → team → agent tree (World) or the flat agents list. */}
|
||||||
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={isClaw ? "claw" : "*"} selectedIds={selectedAgents} onToggleSelect={(id) => setSelectedAgents((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); return next; } const lv = nodeLevel.get(id); const curLv = prev.size ? nodeLevel.get([...prev][0]) : lv; if (lv !== curLv) return new Set([id]); next.add(id); return next; })} />
|
<StructureTree roots={treeRoots} activeId={treeActiveId} autoExpand={treeAutoExpand} onSelectNode={onTreeSelect} selectMode={selectMode} selectLevel={"*"} selectedIds={selectedAgents} onToggleSelect={(id) => setSelectedAgents((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); return next; } const lv = nodeLevel.get(id); const curLv = prev.size ? nodeLevel.get([...prev][0]) : lv; if (lv !== curLv) return new Set([id]); next.add(id); return next; })} />
|
||||||
{selectMode ? (
|
{selectMode ? (
|
||||||
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
|
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
<div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div>
|
<div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// Streams the deletion of selected entities. Agents hard-purge with full reaping
|
// Streams the cascade-deletion of selected entities. Every kind — agents,
|
||||||
// (POST /api/claws/batch-delete, SSE: runtime/containers/files/DB). Teams,
|
// teams, companies, orgs — routes through the same SSE endpoint
|
||||||
// companies and orgs are structural deletes (DELETE /api/<kind>/{id} per item —
|
// (POST /api/claws/batch-delete). Server expands groups to their descendant
|
||||||
// the children survive, just ungrouped). On done, the sidebar refreshes.
|
// agents, hard-reaps each (runtime/containers/files/DB), then deletes the
|
||||||
|
// group rows themselves. No orphans left behind.
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
@@ -15,7 +16,7 @@ type Item = { id: string; name: string };
|
|||||||
type Line = { stage: string; label: string };
|
type Line = { stage: string; label: string };
|
||||||
|
|
||||||
const NOUN: Record<ReapKind, string> = { agents: "agent", teams: "team", companies: "company", orgs: "organization" };
|
const NOUN: Record<ReapKind, string> = { agents: "agent", teams: "team", companies: "company", orgs: "organization" };
|
||||||
const ENDPOINT: Record<Exclude<ReapKind, "agents">, string> = { teams: "teams", companies: "companies", orgs: "orgs" };
|
const BODY_KEY: Record<ReapKind, string> = { agents: "ids", teams: "teams", companies: "companies", orgs: "orgs" };
|
||||||
|
|
||||||
function lineColor(stage: string): string {
|
function lineColor(stage: string): string {
|
||||||
if (stage === "removed" || stage === "done") return "#7fd0a0";
|
if (stage === "removed" || stage === "done") return "#7fd0a0";
|
||||||
@@ -41,9 +42,11 @@ export function ReapProgressModal({ items, kind, onClose, onDone }: { items: Ite
|
|||||||
started.current = true;
|
started.current = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (kind === "agents") {
|
// One SSE stream regardless of kind. The backend expands
|
||||||
// Full reap via the streaming batch endpoint.
|
// teams/companies/orgs to the agents inside them, reaps each, then
|
||||||
const res = await fetch("/api/claws/batch-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: items.map((i) => i.id) }) });
|
// deletes the group rows.
|
||||||
|
const body: Record<string, string[]> = { [BODY_KEY[kind]]: items.map((i) => i.id) };
|
||||||
|
const res = await fetch("/api/claws/batch-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
||||||
if (!res.ok || !res.body) { setError(`Request failed (${res.status})`); setFinished(true); return; }
|
if (!res.ok || !res.body) { setError(`Request failed (${res.status})`); setFinished(true); return; }
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
const dec = new TextDecoder();
|
const dec = new TextDecoder();
|
||||||
@@ -65,20 +68,6 @@ export function ReapProgressModal({ items, kind, onClose, onDone }: { items: Ite
|
|||||||
} catch { /* skip */ }
|
} catch { /* skip */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Structural delete, one request per item.
|
|
||||||
const ep = ENDPOINT[kind];
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const it = items[i];
|
|
||||||
setLines((p) => [...p, { stage: "start", label: `Removing ${it.name}…` }]);
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/${ep}/${it.id}`, { method: "DELETE" });
|
|
||||||
if (res.ok) setLines((p) => [...p, { stage: "removed", label: `✓ ${it.name} removed` }]);
|
|
||||||
else setLines((p) => [...p, { stage: "error", label: `${it.name}: failed (${res.status})` }]);
|
|
||||||
} catch { setLines((p) => [...p, { stage: "error", label: `${it.name}: network error` }]); }
|
|
||||||
setPct(Math.round((100 * (i + 1)) / Math.max(1, items.length)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFinished(true);
|
setFinished(true);
|
||||||
} catch { setError("Network error"); setFinished(true); }
|
} catch { setError("Network error"); setFinished(true); }
|
||||||
})();
|
})();
|
||||||
@@ -90,8 +79,8 @@ export function ReapProgressModal({ items, kind, onClose, onDone }: { items: Ite
|
|||||||
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 11, padding: "16px 20px", borderBottom: "1px solid rgba(255,255,255,.07)" }}>
|
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 11, padding: "16px 20px", borderBottom: "1px solid rgba(255,255,255,.07)" }}>
|
||||||
<span style={{ width: 34, height: 34, flex: "none", borderRadius: 9, background: "rgba(255,111,97,.12)", border: "1px solid rgba(255,111,97,.3)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff6f61" }}><Trash2 size={16} /></span>
|
<span style={{ width: 34, height: 34, flex: "none", borderRadius: 9, background: "rgba(255,111,97,.12)", border: "1px solid rgba(255,111,97,.3)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff6f61" }}><Trash2 size={16} /></span>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 16, fontWeight: 700, color: "#f3f3f5" }}>{finished ? "Removal complete" : `Removing ${count} ${NOUN[kind]}${count === 1 ? "" : "s"}…`}</div>
|
<div style={{ fontSize: 16, fontWeight: 700, color: "#f3f3f5" }}>{finished ? "Removal complete" : `Reaping ${count} ${NOUN[kind]}${count === 1 ? "" : "s"}…`}</div>
|
||||||
<div style={{ fontSize: 12, color: "#8a8a92", marginTop: 2 }}>{kind === "agents" ? "Reaping runtime, containers, files & data — this is permanent." : "Removing the grouping — the items inside survive, just ungrouped."}</div>
|
<div style={{ fontSize: 12, color: "#8a8a92", marginTop: 2 }}>{kind === "agents" ? "Reaping runtime, containers, files & data — this is permanent." : `Cascading through every agent inside — runtime, containers, files & data all reaped. Permanent.`}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user