mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
docker swarm stack read apis
This commit is contained in:
@@ -93,6 +93,8 @@ enum ReadRequest {
|
||||
InspectSwarmSecret(InspectSwarmSecret),
|
||||
ListSwarmConfigs(ListSwarmConfigs),
|
||||
InspectSwarmConfig(InspectSwarmConfig),
|
||||
ListSwarmStacks(ListSwarmStacks),
|
||||
InspectSwarmStack(InspectSwarmStack),
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary(GetServersSummary),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ pub enum PeripheryRequest {
|
||||
InspectSwarmTask(InspectSwarmTask),
|
||||
InspectSwarmSecret(InspectSwarmSecret),
|
||||
InspectSwarmConfig(InspectSwarmConfig),
|
||||
InspectSwarmStack(InspectSwarmStack),
|
||||
|
||||
// Terminal
|
||||
ListTerminals(ListTerminals),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use komodo_client::entities::{
|
||||
|
||||
pub mod compose;
|
||||
pub mod config;
|
||||
pub mod stack;
|
||||
pub mod stats;
|
||||
|
||||
mod container;
|
||||
|
||||
93
bin/periphery/src/docker/stack.rs
Normal file
93
bin/periphery/src/docker/stack.rs
Normal 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")
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
124
client/core/rs/src/entities/docker/stack.rs
Normal file
124
client/core/rs/src/entities/docker/stack.rs
Normal 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>,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
2
frontend/public/client/responses.d.ts
vendored
2
frontend/public/client/responses.d.ts
vendored
@@ -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;
|
||||
|
||||
100
frontend/public/client/types.d.ts
vendored
100
frontend/public/client/types.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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} />
|
||||
|
||||
76
frontend/src/components/resources/swarm/info/stacks.tsx
Normal file
76
frontend/src/components/resources/swarm/info/stacks.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
76
frontend/src/pages/swarm/stack.tsx
Normal file
76
frontend/src/pages/swarm/stack.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user