docker swarm stack read apis

This commit is contained in:
mbecker20
2025-11-22 16:05:51 -08:00
parent 8341c6b802
commit b1fec7c663
25 changed files with 747 additions and 26 deletions

View File

@@ -93,6 +93,8 @@ enum ReadRequest {
InspectSwarmSecret(InspectSwarmSecret),
ListSwarmConfigs(ListSwarmConfigs),
InspectSwarmConfig(InspectSwarmConfig),
ListSwarmStacks(ListSwarmStacks),
InspectSwarmStack(InspectSwarmStack),
// ==== SERVER ====
GetServersSummary(GetServersSummary),

View File

@@ -377,3 +377,46 @@ impl Resolve<ReadArgs> for InspectSwarmConfig {
.map_err(Into::into)
}
}
impl Resolve<ReadArgs> for ListSwarmStacks {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<ListSwarmStacksResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
let cache =
swarm_status_cache().get_or_insert_default(&swarm.id).await;
if let Some(lists) = &cache.lists {
Ok(lists.stacks.clone())
} else {
Ok(Vec::new())
}
}
}
impl Resolve<ReadArgs> for InspectSwarmStack {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<InspectSwarmStackResponse> {
let swarm = get_check_permissions::<Swarm>(
&self.swarm,
user,
PermissionLevel::Read.into(),
)
.await?;
swarm_request(
&swarm.config.server_ids,
periphery_client::api::swarm::InspectSwarmStack {
name: self.stack,
},
)
.await
.map_err(Into::into)
}
}

View File

@@ -143,6 +143,7 @@ pub enum PeripheryRequest {
InspectSwarmTask(InspectSwarmTask),
InspectSwarmSecret(InspectSwarmSecret),
InspectSwarmConfig(InspectSwarmConfig),
InspectSwarmStack(InspectSwarmStack),
// Terminal
ListTerminals(ListTerminals),

View File

@@ -1,13 +1,17 @@
use anyhow::Context as _;
use komodo_client::entities::docker::{
SwarmLists, config::SwarmConfig, node::SwarmNode,
secret::SwarmSecret, service::SwarmService, task::SwarmTask,
secret::SwarmSecret, service::SwarmService, stack::SwarmStackLists,
task::SwarmTask,
};
use periphery_client::api::swarm::*;
use resolver_api::Resolve;
use crate::{
docker::config::{inspect_swarm_config, list_swarm_configs},
docker::{
config::{inspect_swarm_config, list_swarm_configs},
stack::{inspect_swarm_stack, list_swarm_stacks},
},
state::docker_client,
};
@@ -21,13 +25,14 @@ impl Resolve<super::Args> for PollSwarmStatus {
.iter()
.next()
.context("Could not connect to docker client")?;
let (inspect, nodes, services, tasks, secrets, configs) = tokio::join!(
let (inspect, nodes, services, tasks, secrets, configs, stacks) = tokio::join!(
client.inspect_swarm(),
client.list_swarm_nodes(),
client.list_swarm_services(),
client.list_swarm_tasks(),
client.list_swarm_secrets(),
list_swarm_configs(),
list_swarm_stacks(),
);
Ok(PollSwarmStatusResponse {
inspect: inspect.ok(),
@@ -37,6 +42,7 @@ impl Resolve<super::Args> for PollSwarmStatus {
tasks: tasks.unwrap_or_default(),
secrets: secrets.unwrap_or_default(),
configs: configs.unwrap_or_default(),
stacks: stacks.unwrap_or_default(),
},
})
}
@@ -126,3 +132,16 @@ impl Resolve<super::Args> for InspectSwarmConfig {
inspect_swarm_config(&self.id).await
}
}
// =======
// Stack
// =======
impl Resolve<super::Args> for InspectSwarmStack {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<SwarmStackLists> {
inspect_swarm_stack(self.name).await
}
}

View File

@@ -9,6 +9,7 @@ use komodo_client::entities::{
pub mod compose;
pub mod config;
pub mod stack;
pub mod stats;
mod container;

View File

@@ -0,0 +1,93 @@
use anyhow::{Context, anyhow};
use command::run_komodo_standard_command;
use komodo_client::entities::docker::stack::{
SwarmStackListItem, SwarmStackLists, SwarmStackServiceListItem,
SwarmStackTaskListItem,
};
pub async fn inspect_swarm_stack(
name: String,
) -> anyhow::Result<SwarmStackLists> {
let (services, tasks) = tokio::try_join!(
list_swarm_stack_services(&name),
list_swarm_stack_tasks(&name),
)?;
Ok(SwarmStackLists {
name,
services,
tasks,
})
}
pub async fn list_swarm_stacks()
-> anyhow::Result<Vec<SwarmStackListItem>> {
let res = run_komodo_standard_command(
"List Swarm Stacks",
None,
"docker stack ls --format json",
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(
"Failed to list swarm stacks using 'docker stack ls'",
));
}
// The output is in JSONL, need to convert to standard JSON vec.
serde_json::from_str(&format!(
"[{}]",
res.stdout.trim().replace('\n', ",")
))
.context("Failed to parse 'docker stack ls' response from json")
}
pub async fn list_swarm_stack_services(
stack: &str,
) -> anyhow::Result<Vec<SwarmStackServiceListItem>> {
let res = run_komodo_standard_command(
"List Swarm Stack Services",
None,
format!("docker stack services --format json {stack}"),
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(
"Failed to list swarm stacks using 'docker stack services'",
));
}
// The output is in JSONL, need to convert to standard JSON vec.
serde_json::from_str(&format!(
"[{}]",
res.stdout.trim().replace('\n', ",")
))
.context(
"Failed to parse 'docker stack services' response from json",
)
}
pub async fn list_swarm_stack_tasks(
stack: &str,
) -> anyhow::Result<Vec<SwarmStackTaskListItem>> {
let res = run_komodo_standard_command(
"List Swarm Stack Tasks",
None,
format!("docker stack ps --format json {stack}"),
)
.await;
if !res.success {
return Err(anyhow!("{}", res.combined()).context(
"Failed to list swarm stacks using 'docker stack ps'",
));
}
// The output is in JSONL, need to convert to standard JSON vec.
serde_json::from_str(&format!(
"[{}]",
res.stdout.trim().replace('\n', ",")
))
.context("Failed to parse 'docker stack ps' response from json")
}

View File

@@ -9,6 +9,7 @@ use crate::entities::{
node::{SwarmNode, SwarmNodeListItem},
secret::{SwarmSecret, SwarmSecretListItem},
service::{SwarmService, SwarmServiceListItem},
stack::{SwarmStackListItem, SwarmStackLists},
swarm::SwarmInspectInfo,
task::{SwarmTask, SwarmTaskListItem},
},
@@ -349,3 +350,45 @@ pub struct InspectSwarmConfig {
#[typeshare]
pub type InspectSwarmConfigResponse = Vec<SwarmConfig>;
//
/// List stacks on the target Swarm.
/// Response: [ListSwarmStacksResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListSwarmStacksResponse)]
#[error(serror::Error)]
pub struct ListSwarmStacks {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
}
#[typeshare]
pub type ListSwarmStacksResponse = Vec<SwarmStackListItem>;
//
/// Inspect a stack on the target Swarm.
/// Response: [SwarmStackLists].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(InspectSwarmStackResponse)]
#[error(serror::Error)]
pub struct InspectSwarmStack {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub swarm: String,
/// Swarm stack name
pub stack: String,
}
#[typeshare]
pub type InspectSwarmStackResponse = SwarmStackLists;

View File

@@ -6,7 +6,7 @@ use typeshare::typeshare;
use super::*;
/// Swarm config list item.
/// Returned by `docker swarm config ls --format json`
/// Returned by `docker config ls --format json`
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,

View File

@@ -8,8 +8,8 @@ use crate::entities::{
config::SwarmConfigListItem, container::ContainerListItem,
image::ImageListItem, network::NetworkListItem,
node::SwarmNodeListItem, secret::SwarmSecretListItem,
service::SwarmServiceListItem, task::SwarmTaskListItem,
volume::VolumeListItem,
service::SwarmServiceListItem, stack::SwarmStackListItem,
task::SwarmTaskListItem, volume::VolumeListItem,
},
stack::ComposeProject,
};
@@ -23,6 +23,7 @@ pub mod network;
pub mod node;
pub mod secret;
pub mod service;
pub mod stack;
pub mod stats;
pub mod swarm;
pub mod task;
@@ -36,6 +37,7 @@ pub struct SwarmLists {
pub tasks: Vec<SwarmTaskListItem>,
pub secrets: Vec<SwarmSecretListItem>,
pub configs: Vec<SwarmConfigListItem>,
pub stacks: Vec<SwarmStackListItem>,
}
/// Standard docker lists available from a Server.

View File

@@ -0,0 +1,124 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
/// Swarm stack list item.
/// Returned by `docker stack ls --format json`
///
/// https://docs.docker.com/reference/cli/docker/stack/ls/#format
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackListItem {
/// Swarm stack name.
#[serde(rename = "Name")]
pub name: Option<String>,
/// Number of services which are part of the stack
#[serde(rename = "Services")]
pub services: Option<String>,
/// The stack orchestrator
#[serde(rename = "Orchestrator")]
pub orchestrator: Option<String>,
/// The stack namespace
#[serde(rename = "Namespace")]
pub namespace: Option<String>,
}
/// All entities related to docker stack available over CLI.
/// Returned by:
/// ```
/// docker stack services --format json <STACK>
/// docker stack ps --format json <STACK>
/// ```
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackLists {
/// Swarm stack name.
#[serde(rename = "Name")]
pub name: String,
/// Services part of the stack
#[serde(rename = "Services")]
pub services: Vec<SwarmStackServiceListItem>,
/// Tasks part of the stack
#[serde(rename = "Tasks")]
pub tasks: Vec<SwarmStackTaskListItem>,
}
/// Swarm stack service list item.
/// Returned by `docker stack services --format json <NAME>`
///
/// https://docs.docker.com/reference/cli/docker/stack/services/#format
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackServiceListItem {
#[serde(rename = "ID")]
pub id: Option<String>,
/// Swarm stack task name.
#[serde(rename = "Name")]
pub name: Option<String>,
/// The service mode.
#[serde(rename = "Mode")]
pub mode: Option<String>,
/// The service replicas, formatted as string.
#[serde(rename = "Replicas")]
pub replicas: Option<String>,
/// The image associated with service
#[serde(rename = "Image")]
pub image: Option<String>,
/// Task exposed ports, formatted as a string.
#[serde(rename = "Ports")]
pub ports: Option<String>,
}
/// Swarm stack task list item.
/// Returned by `docker stack ps --format json <NAME>`
///
/// https://docs.docker.com/reference/cli/docker/stack/ps/#format
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct SwarmStackTaskListItem {
#[serde(rename = "ID")]
pub id: Option<String>,
/// Swarm stack task name.
#[serde(rename = "Name")]
pub name: Option<String>,
/// The image associated with task
#[serde(rename = "Image")]
pub image: Option<String>,
/// The node the task is running on
#[serde(rename = "Node")]
pub node: Option<String>,
#[serde(rename = "DesiredState")]
pub desired_state: Option<String>,
#[serde(rename = "CurrentState")]
pub current_state: Option<String>,
/// An error message, if one exists
#[serde(rename = "Error")]
pub error: Option<String>,
/// Task exposed ports, formatted as a string.
#[serde(rename = "Ports")]
pub ports: Option<String>,
}

View File

@@ -39,6 +39,8 @@ export type ReadResponses = {
InspectSwarmSecret: Types.InspectSwarmSecretResponse;
ListSwarmConfigs: Types.ListSwarmConfigsResponse;
InspectSwarmConfig: Types.InspectSwarmConfigResponse;
ListSwarmStacks: Types.ListSwarmStacksResponse;
InspectSwarmStack: Types.InspectSwarmStackResponse;
// ==== SERVER ====
GetServersSummary: Types.GetServersSummaryResponse;

View File

@@ -4332,6 +4332,67 @@ export interface SwarmService {
export type InspectSwarmServiceResponse = SwarmService;
/**
* Swarm stack service list item.
* Returned by `docker stack services --format json <NAME>`
*
* https://docs.docker.com/reference/cli/docker/stack/services/#format
*/
export interface SwarmStackServiceListItem {
ID?: string;
/** Swarm stack task name. */
Name?: string;
/** The service mode. */
Mode?: string;
/** The service replicas, formatted as string. */
Replicas?: string;
/** The image associated with service */
Image?: string;
/** Task exposed ports, formatted as a string. */
Ports?: string;
}
/**
* Swarm stack task list item.
* Returned by `docker stack ps --format json <NAME>`
*
* https://docs.docker.com/reference/cli/docker/stack/ps/#format
*/
export interface SwarmStackTaskListItem {
ID?: string;
/** Swarm stack task name. */
Name?: string;
/** The image associated with task */
Image?: string;
/** The node the task is running on */
Node?: string;
DesiredState?: string;
CurrentState?: string;
/** An error message, if one exists */
Error?: string;
/** Task exposed ports, formatted as a string. */
Ports?: string;
}
/**
* All entities related to docker stack available over CLI.
* Returned by:
* ```
* docker stack services --format json <STACK>
* docker stack ps --format json <STACK>
* ```
*/
export interface SwarmStackLists {
/** Swarm stack name. */
Name: string;
/** Services part of the stack */
Services: SwarmStackServiceListItem[];
/** Tasks part of the stack */
Tasks: SwarmStackTaskListItem[];
}
export type InspectSwarmStackResponse = SwarmStackLists;
export enum TaskState {
NEW = "new",
ALLOCATED = "allocated",
@@ -4975,7 +5036,7 @@ export type ListStacksResponse = StackListItem[];
/**
* Swarm config list item.
* Returned by `docker swarm config ls --format json`
* Returned by `docker config ls --format json`
*/
export interface SwarmConfigListItem {
/** User-defined name of the config. */
@@ -5049,6 +5110,25 @@ export interface SwarmServiceListItem {
export type ListSwarmServicesResponse = SwarmServiceListItem[];
/**
* Swarm stack list item.
* Returned by `docker stack ls --format json`
*
* https://docs.docker.com/reference/cli/docker/stack/ls/#format
*/
export interface SwarmStackListItem {
/** Swarm stack name. */
Name?: string;
/** Number of services which are part of the stack */
Services?: string;
/** The stack orchestrator */
Orchestrator?: string;
/** The stack namespace */
Namespace?: string;
}
export type ListSwarmStacksResponse = SwarmStackListItem[];
/** Swarm task list item. */
export interface SwarmTaskListItem {
/** The ID of the task. */
@@ -7745,6 +7825,17 @@ export interface InspectSwarmService {
service: string;
}
/**
* Inspect a stack on the target Swarm.
* Response: [SwarmStackLists].
*/
export interface InspectSwarmStack {
/** Id or name */
swarm: string;
/** Swarm stack name */
stack: string;
}
/**
* Inspect a Swarm task.
* Response: [SwarmTask].
@@ -8209,6 +8300,15 @@ export interface ListSwarmServices {
swarm: string;
}
/**
* List stacks on the target Swarm.
* Response: [ListSwarmStacksResponse].
*/
export interface ListSwarmStacks {
/** Id or name */
swarm: string;
}
/**
* List tasks on the target Swarm.
* Response: [ListSwarmTasksResponse].
@@ -9993,6 +10093,8 @@ export type ReadRequest =
| { type: "InspectSwarmSecret", params: InspectSwarmSecret }
| { type: "ListSwarmConfigs", params: ListSwarmConfigs }
| { type: "InspectSwarmConfig", params: InspectSwarmConfig }
| { type: "ListSwarmStacks", params: ListSwarmStacks }
| { type: "InspectSwarmStack", params: InspectSwarmStack }
| { type: "GetServersSummary", params: GetServersSummary }
| { type: "GetServer", params: GetServer }
| { type: "GetServerState", params: GetServerState }

View File

@@ -1,6 +1,6 @@
use komodo_client::entities::docker::{
SwarmLists, config::SwarmConfig, node::SwarmNode,
secret::SwarmSecret, service::SwarmService,
secret::SwarmSecret, service::SwarmService, stack::SwarmStackLists,
swarm::SwarmInspectInfo, task::SwarmTask,
};
use resolver_api::Resolve;
@@ -72,3 +72,15 @@ pub struct InspectSwarmSecret {
pub struct InspectSwarmConfig {
pub id: String,
}
// =======
// Stack
// =======
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(SwarmStackLists)]
#[error(anyhow::Error)]
pub struct InspectSwarmStack {
/// The swarm stack name
pub name: String,
}

View File

@@ -34,6 +34,8 @@ export type ReadResponses = {
InspectSwarmSecret: Types.InspectSwarmSecretResponse;
ListSwarmConfigs: Types.ListSwarmConfigsResponse;
InspectSwarmConfig: Types.InspectSwarmConfigResponse;
ListSwarmStacks: Types.ListSwarmStacksResponse;
InspectSwarmStack: Types.InspectSwarmStackResponse;
GetServersSummary: Types.GetServersSummaryResponse;
GetServer: Types.GetServerResponse;
GetServerState: Types.GetServerStateResponse;

View File

@@ -4288,6 +4288,63 @@ export interface SwarmService {
JobStatus?: ServiceJobStatus;
}
export type InspectSwarmServiceResponse = SwarmService;
/**
* Swarm stack service list item.
* Returned by `docker stack services --format json <NAME>`
*
* https://docs.docker.com/reference/cli/docker/stack/services/#format
*/
export interface SwarmStackServiceListItem {
ID?: string;
/** Swarm stack task name. */
Name?: string;
/** The service mode. */
Mode?: string;
/** The service replicas, formatted as string. */
Replicas?: string;
/** The image associated with service */
Image?: string;
/** Task exposed ports, formatted as a string. */
Ports?: string;
}
/**
* Swarm stack task list item.
* Returned by `docker stack ps --format json <NAME>`
*
* https://docs.docker.com/reference/cli/docker/stack/ps/#format
*/
export interface SwarmStackTaskListItem {
ID?: string;
/** Swarm stack task name. */
Name?: string;
/** The image associated with task */
Image?: string;
/** The node the task is running on */
Node?: string;
DesiredState?: string;
CurrentState?: string;
/** An error message, if one exists */
Error?: string;
/** Task exposed ports, formatted as a string. */
Ports?: string;
}
/**
* All entities related to docker stack available over CLI.
* Returned by:
* ```
* docker stack services --format json <STACK>
* docker stack ps --format json <STACK>
* ```
*/
export interface SwarmStackLists {
/** Swarm stack name. */
Name: string;
/** Services part of the stack */
Services: SwarmStackServiceListItem[];
/** Tasks part of the stack */
Tasks: SwarmStackTaskListItem[];
}
export type InspectSwarmStackResponse = SwarmStackLists;
export declare enum TaskState {
NEW = "new",
ALLOCATED = "allocated",
@@ -4852,7 +4909,7 @@ export type StackListItem = ResourceListItem<StackListItemInfo>;
export type ListStacksResponse = StackListItem[];
/**
* Swarm config list item.
* Returned by `docker swarm config ls --format json`
* Returned by `docker config ls --format json`
*/
export interface SwarmConfigListItem {
/** User-defined name of the config. */
@@ -4918,6 +4975,23 @@ export interface SwarmServiceListItem {
UpdatedAt?: string;
}
export type ListSwarmServicesResponse = SwarmServiceListItem[];
/**
* Swarm stack list item.
* Returned by `docker stack ls --format json`
*
* https://docs.docker.com/reference/cli/docker/stack/ls/#format
*/
export interface SwarmStackListItem {
/** Swarm stack name. */
Name?: string;
/** Number of services which are part of the stack */
Services?: string;
/** The stack orchestrator */
Orchestrator?: string;
/** The stack namespace */
Namespace?: string;
}
export type ListSwarmStacksResponse = SwarmStackListItem[];
/** Swarm task list item. */
export interface SwarmTaskListItem {
/** The ID of the task. */
@@ -7349,6 +7423,16 @@ export interface InspectSwarmService {
/** Service id */
service: string;
}
/**
* Inspect a stack on the target Swarm.
* Response: [SwarmStackLists].
*/
export interface InspectSwarmStack {
/** Id or name */
swarm: string;
/** Swarm stack name */
stack: string;
}
/**
* Inspect a Swarm task.
* Response: [SwarmTask].
@@ -7761,6 +7845,14 @@ export interface ListSwarmServices {
/** Id or name */
swarm: string;
}
/**
* List stacks on the target Swarm.
* Response: [ListSwarmStacksResponse].
*/
export interface ListSwarmStacks {
/** Id or name */
swarm: string;
}
/**
* List tasks on the target Swarm.
* Response: [ListSwarmTasksResponse].
@@ -9595,6 +9687,12 @@ export type ReadRequest = {
} | {
type: "InspectSwarmConfig";
params: InspectSwarmConfig;
} | {
type: "ListSwarmStacks";
params: ListSwarmStacks;
} | {
type: "InspectSwarmStack";
params: InspectSwarmStack;
} | {
type: "GetServersSummary";
params: GetServersSummary;

View File

@@ -7,6 +7,7 @@ import {
KeyRound,
ListTodo,
Settings,
SquareStack,
} from "lucide-react";
import { DeleteResource, NewResource, ResourcePageHeader } from "../common";
import { SwarmTable } from "./table";
@@ -138,7 +139,8 @@ export type SwarmResourceType =
| "Service"
| "Task"
| "Secret"
| "Config";
| "Config"
| "Stack";
export const SWARM_ICONS: {
[type in SwarmResourceType]: React.FC<{ size?: number }>;
@@ -148,6 +150,7 @@ export const SWARM_ICONS: {
Task: ({ size }) => <ListTodo className={`w-${size} h-${size}`} />,
Secret: ({ size }) => <KeyRound className={`w-${size} h-${size}`} />,
Config: ({ size }) => <Settings className={`w-${size} h-${size}`} />,
Stack: ({ size }) => <SquareStack className={`w-${size} h-${size}`} />,
};
export const SwarmLink = ({

View File

@@ -13,10 +13,12 @@ import { SwarmServices } from "./services";
import { SwarmTasks } from "./tasks";
import { SwarmInspect } from "./inspect";
import { SwarmConfigs } from "./configs";
import { SwarmStacks } from "./stacks";
type SwarmInfoView =
| "Inspect"
| "Nodes"
| "Stacks"
| "Services"
| "Tasks"
| "Secrets"
@@ -54,6 +56,9 @@ export const SwarmInfo = ({
{
value: "Nodes",
},
{
value: "Stacks",
},
{
value: "Services",
},
@@ -85,6 +90,8 @@ export const SwarmInfo = ({
return <SwarmInspect id={id} titleOther={Selector} />;
case "Nodes":
return <SwarmNodes id={id} titleOther={Selector} _search={_search} />;
case "Stacks":
return <SwarmStacks id={id} titleOther={Selector} _search={_search} />;
case "Services":
return (
<SwarmServices id={id} titleOther={Selector} _search={_search} />

View File

@@ -0,0 +1,76 @@
import { Section } from "@components/layouts";
import { useRead } from "@lib/hooks";
import { DataTable, SortableHeader } from "@ui/data-table";
import { Dispatch, ReactNode, SetStateAction } from "react";
import { Search } from "lucide-react";
import { Input } from "@ui/input";
import { filterBySplit } from "@lib/utils";
import { SwarmLink } from "..";
export const SwarmStacks = ({
id,
titleOther,
_search,
}: {
id: string;
titleOther: ReactNode;
_search: [string, Dispatch<SetStateAction<string>>];
}) => {
const [search, setSearch] = _search;
const stacks =
useRead("ListSwarmStacks", { swarm: id }, { refetchInterval: 10_000 })
.data ?? [];
const filtered = filterBySplit(
stacks,
search,
(stack) => stack.Name ?? "Unknown"
);
return (
<Section
titleOther={titleOther}
actions={
<div className="flex items-center gap-4 flex-wrap">
<div className="relative">
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="search..."
className="pl-8 w-[200px] lg:w-[300px]"
/>
</div>
</div>
}
>
<DataTable
containerClassName="min-h-[60vh]"
tableKey="swarm-stacks"
data={filtered}
columns={[
{
accessorKey: "Name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<SwarmLink
type="Stack"
swarm_id={id}
resource_id={row.original.Name}
name={row.original.Name}
/>
),
},
{
accessorKey: "Services",
header: ({ column }) => (
<SortableHeader column={column} title="Services" />
),
},
]}
/>
</Section>
);
};

View File

@@ -2,10 +2,10 @@ import { ResourceLink } from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, KeyRound, Loader2 } from "lucide-react";
import { ChevronLeft, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { useSwarm } from "@components/resources/swarm";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
export default function SwarmConfigPage() {
const { id, config: __config } = useParams() as {
@@ -36,6 +36,8 @@ export default function SwarmConfigPage() {
return <div className="flex w-full py-4">Failed to inspect config.</div>;
}
const Icon = SWARM_ICONS.Config;
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
@@ -54,7 +56,7 @@ export default function SwarmConfigPage() {
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<KeyRound className="w-8 h-8" />
<Icon size={8} />
</div>
<PageHeaderName name={config?.Spec?.Name ?? config?.ID} />
</div>

View File

@@ -2,10 +2,10 @@ import { ResourceLink } from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, Diamond, Loader2 } from "lucide-react";
import { ChevronLeft, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { useSwarm } from "@components/resources/swarm";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
export default function SwarmNodePage() {
const { id, node: __node } = useParams() as {
@@ -35,6 +35,8 @@ export default function SwarmNodePage() {
return <div className="flex w-full py-4">Failed to inspect node.</div>;
}
const Icon = SWARM_ICONS.Node;
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
@@ -53,7 +55,7 @@ export default function SwarmNodePage() {
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<Diamond className="w-8 h-8" />
<Icon size={8} />
</div>
<PageHeaderName
name={node.Spec?.Name ?? node.Description?.Hostname ?? node.ID}

View File

@@ -2,10 +2,10 @@ import { ResourceLink } from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, KeyRound, Loader2 } from "lucide-react";
import { ChevronLeft, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { useSwarm } from "@components/resources/swarm";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
export default function SwarmSecretPage() {
const { id, secret: __secret } = useParams() as {
@@ -35,6 +35,8 @@ export default function SwarmSecretPage() {
return <div className="flex w-full py-4">Failed to inspect secret.</div>;
}
const Icon = SWARM_ICONS.Secret;
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
@@ -53,7 +55,7 @@ export default function SwarmSecretPage() {
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<KeyRound className="w-8 h-8" />
<Icon size={8} />
</div>
<PageHeaderName name={secret?.Spec?.Name ?? secret?.ID} />
</div>

View File

@@ -2,10 +2,10 @@ import { ResourceLink } from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, FolderCode, Loader2 } from "lucide-react";
import { ChevronLeft, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { useSwarm } from "@components/resources/swarm";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
export default function SwarmServicePage() {
const { id, service: __service } = useParams() as {
@@ -35,6 +35,8 @@ export default function SwarmServicePage() {
return <div className="flex w-full py-4">Failed to inspect service.</div>;
}
const Icon = SWARM_ICONS.Service;
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
@@ -53,7 +55,7 @@ export default function SwarmServicePage() {
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<FolderCode className="w-8 h-8" />
<Icon size={8} />
</div>
<PageHeaderName name={service.Spec?.Name ?? service.ID} />
</div>

View File

@@ -0,0 +1,76 @@
import { ResourceLink } from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
export default function SwarmStackPage() {
const { id, stack: __stack } = useParams() as {
id: string;
stack: string;
};
const _stack = decodeURIComponent(__stack);
console.log(_stack);
const swarm = useSwarm(id);
const { data: stack, isPending } = useRead("InspectSwarmStack", {
swarm: id,
stack: _stack,
});
useSetTitle(`${swarm?.name} | Stack | ${stack?.Name ?? "Unknown"}`);
const nav = useNavigate();
if (isPending) {
return (
<div className="flex justify-center w-full py-4">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
if (!stack) {
return <div className="flex w-full py-4">Failed to inspect stack.</div>;
}
const Icon = SWARM_ICONS.Stack;
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
<div className="flex flex-col gap-4">
{/* BACK */}
<div className="flex items-center justify-between mb-4">
<Button
className="gap-2"
variant="secondary"
onClick={() => nav("/swarms/" + id)}
>
<ChevronLeft className="w-4" /> Back
</Button>
</div>
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<Icon size={8} />
</div>
<PageHeaderName name={stack?.Name} />
</div>
{/* INFO */}
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
Swarm Stack
<ResourceLink type="Swarm" id={id} />
</div>
</div>
<MonacoEditor
value={JSON.stringify(stack, null, 2)}
language="json"
readOnly
/>
</div>
);
}

View File

@@ -2,10 +2,10 @@ import { ResourceLink } from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, ListTodo, Loader2 } from "lucide-react";
import { ChevronLeft, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { useSwarm } from "@components/resources/swarm";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
export default function SwarmTaskPage() {
const { id, task: __task } = useParams() as {
@@ -33,6 +33,8 @@ export default function SwarmTaskPage() {
return <div className="flex w-full py-4">Failed to inspect task.</div>;
}
const Icon = SWARM_ICONS.Task;
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
@@ -51,7 +53,7 @@ export default function SwarmTaskPage() {
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<ListTodo className="w-8 h-8" />
<Icon size={8} />
</div>
<PageHeaderName name={task.ID} />
</div>

View File

@@ -1,6 +1,5 @@
import { Layout } from "@components/layouts";
import { LOGIN_TOKENS, useAuth, useUser } from "@lib/hooks";
import SwarmConfigPage from "@pages/swarm/config";
import { Loader2 } from "lucide-react";
import { lazy, Suspense } from "react";
import {
@@ -37,6 +36,8 @@ const SwarmNodePage = lazy(() => import("@pages/swarm/node"));
const SwarmServicePage = lazy(() => import("@pages/swarm/service"));
const SwarmTaskPage = lazy(() => import("@pages/swarm/task"));
const SwarmSecretPage = lazy(() => import("@pages/swarm/secret"));
const SwarmConfigPage = lazy(() => import("@pages/swarm/config"));
const SwarmStackPage = lazy(() => import("@pages/swarm/stack"));
const sanitize_query = (search: URLSearchParams) => {
search.delete("token");
@@ -148,6 +149,10 @@ export const Router = () => {
path=":id/swarm-config/:config"
element={<SwarmConfigPage />}
/>
<Route
path=":id/swarm-stack/:stack"
element={<SwarmStackPage />}
/>
{/* Terminal Page */}
<Route
path=":id/terminal/:terminal"