mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
improve swarm stack page
This commit is contained in:
@@ -5,8 +5,8 @@ use command::{
|
||||
use komodo_client::entities::{
|
||||
docker::{
|
||||
SwarmLists, config::SwarmConfig, node::SwarmNode,
|
||||
secret::SwarmSecret, service::SwarmService,
|
||||
stack::SwarmStackLists, task::SwarmTask,
|
||||
secret::SwarmSecret, service::SwarmService, stack::SwarmStack,
|
||||
task::SwarmTask,
|
||||
},
|
||||
update::Log,
|
||||
};
|
||||
@@ -144,7 +144,7 @@ impl Resolve<super::Args> for InspectSwarmStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &super::Args,
|
||||
) -> anyhow::Result<SwarmStackLists> {
|
||||
) -> anyhow::Result<SwarmStack> {
|
||||
inspect_swarm_stack(self.stack).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub async fn list_compose_projects()
|
||||
)));
|
||||
}
|
||||
|
||||
let res =
|
||||
let mut res =
|
||||
serde_json::from_str::<Vec<DockerComposeLsItem>>(&res.stdout)
|
||||
.with_context(|| res.stdout.clone())
|
||||
.with_context(|| {
|
||||
@@ -48,7 +48,11 @@ pub async fn list_compose_projects()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
res.sort_by(|a, b| {
|
||||
a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,18 @@ pub async fn list_swarm_configs()
|
||||
}
|
||||
|
||||
// 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 config ls' response from json")
|
||||
let mut res = serde_json::from_str::<Vec<SwarmConfigListItem>>(
|
||||
&format!("[{}]", res.stdout.trim().replace('\n', ",")),
|
||||
)
|
||||
.context("Failed to parse 'docker config ls' response from json")?;
|
||||
|
||||
res.sort_by(|a, b| {
|
||||
a.name
|
||||
.cmp(&b.name)
|
||||
.then_with(|| b.updated_at.cmp(&a.updated_at))
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn inspect_swarm_config(
|
||||
|
||||
@@ -100,6 +100,9 @@ impl DockerClient {
|
||||
container.network_mode =
|
||||
container_id_to_network.get(container_id).cloned();
|
||||
});
|
||||
containers.sort_by(|a, b| {
|
||||
a.state.cmp(&b.state).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
Ok(containers)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ impl DockerClient {
|
||||
&self,
|
||||
containers: &[ContainerListItem],
|
||||
) -> anyhow::Result<Vec<ImageListItem>> {
|
||||
let images = self
|
||||
let mut images = self
|
||||
.docker
|
||||
.list_images(Option::<ListImagesOptions>::None)
|
||||
.await?
|
||||
@@ -37,7 +37,12 @@ impl DockerClient {
|
||||
in_use,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
images.sort_by(|a, b| {
|
||||
a.in_use.cmp(&b.in_use).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(images)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ impl DockerClient {
|
||||
&self,
|
||||
containers: &[ContainerListItem],
|
||||
) -> anyhow::Result<Vec<NetworkListItem>> {
|
||||
let networks = self
|
||||
let mut networks = self
|
||||
.docker
|
||||
.list_networks(Option::<ListNetworksOptions>::None)
|
||||
.await?
|
||||
@@ -54,7 +54,12 @@ impl DockerClient {
|
||||
in_use,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
networks.sort_by(|a, b| {
|
||||
a.in_use.cmp(&b.in_use).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(networks)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,22 @@ impl DockerClient {
|
||||
pub async fn list_swarm_nodes(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmNodeListItem>> {
|
||||
let nodes = self
|
||||
let mut nodes = self
|
||||
.docker
|
||||
.list_nodes(Option::<ListNodesOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm node list")?
|
||||
.into_iter()
|
||||
.map(convert_node_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
nodes.sort_by(|a, b| {
|
||||
a.state
|
||||
.cmp(&b.state)
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
.then_with(|| a.hostname.cmp(&b.hostname))
|
||||
});
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,21 @@ impl DockerClient {
|
||||
pub async fn list_swarm_secrets(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmSecretListItem>> {
|
||||
let secrets = self
|
||||
let mut secrets = self
|
||||
.docker
|
||||
.list_secrets(Option::<ListSecretsOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm secret list")?
|
||||
.into_iter()
|
||||
.map(convert_secret_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
secrets.sort_by(|a, b| {
|
||||
a.name
|
||||
.cmp(&b.name)
|
||||
.then_with(|| b.updated_at.cmp(&a.updated_at))
|
||||
});
|
||||
|
||||
Ok(secrets)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,21 @@ impl DockerClient {
|
||||
pub async fn list_swarm_services(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmServiceListItem>> {
|
||||
let services = self
|
||||
let mut services = self
|
||||
.docker
|
||||
.list_services(Option::<ListServicesOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm service list")?
|
||||
.into_iter()
|
||||
.map(convert_service_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
services.sort_by(|a, b| {
|
||||
a.name
|
||||
.cmp(&b.name)
|
||||
.then_with(|| b.updated_at.cmp(&a.updated_at))
|
||||
});
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use command::run_komodo_standard_command;
|
||||
use komodo_client::entities::docker::stack::{
|
||||
SwarmStackListItem, SwarmStackLists, SwarmStackServiceListItem,
|
||||
SwarmStackTaskListItem,
|
||||
use futures_util::{StreamExt, stream::FuturesOrdered};
|
||||
use komodo_client::entities::{
|
||||
docker::stack::{
|
||||
SwarmStack, SwarmStackListItem, SwarmStackServiceListItem,
|
||||
SwarmStackTaskListItem,
|
||||
},
|
||||
swarm::SwarmState,
|
||||
};
|
||||
|
||||
pub async fn inspect_swarm_stack(
|
||||
name: String,
|
||||
) -> anyhow::Result<SwarmStackLists> {
|
||||
let (services, tasks) = tokio::try_join!(
|
||||
list_swarm_stack_services(&name),
|
||||
) -> anyhow::Result<SwarmStack> {
|
||||
let (tasks, services) = tokio::try_join!(
|
||||
list_swarm_stack_tasks(&name),
|
||||
list_swarm_stack_services(&name)
|
||||
)?;
|
||||
Ok(SwarmStackLists {
|
||||
let state = state_from_tasks(&tasks);
|
||||
Ok(SwarmStack {
|
||||
name,
|
||||
state,
|
||||
services,
|
||||
tasks,
|
||||
})
|
||||
@@ -35,11 +41,35 @@ pub async fn list_swarm_stacks()
|
||||
}
|
||||
|
||||
// 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")
|
||||
let mut stacks = serde_json::from_str::<Vec<SwarmStackListItem>>(
|
||||
&format!("[{}]", res.stdout.trim().replace('\n', ",")),
|
||||
)
|
||||
.context("Failed to parse 'docker stack ls' response from json")?
|
||||
// Attach state concurrently from tasks. Still include stack
|
||||
// if it fails, just with None state.
|
||||
.into_iter()
|
||||
.map(|mut stack| async move {
|
||||
let res = async {
|
||||
let tasks =
|
||||
list_swarm_stack_tasks(stack.name.as_ref()?).await.ok()?;
|
||||
Some(state_from_tasks(&tasks))
|
||||
}
|
||||
.await;
|
||||
if let Some(state) = res {
|
||||
stack.state = Some(state);
|
||||
}
|
||||
stack
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
stacks.sort_by(|a, b| {
|
||||
cmp_option(a.state, b.state)
|
||||
.then_with(|| cmp_option(a.name.as_ref(), b.name.as_ref()))
|
||||
});
|
||||
|
||||
Ok(stacks)
|
||||
}
|
||||
|
||||
pub async fn list_swarm_stack_services(
|
||||
@@ -59,13 +89,18 @@ pub async fn list_swarm_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",
|
||||
)
|
||||
let mut services =
|
||||
serde_json::from_str::<Vec<SwarmStackServiceListItem>>(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context(
|
||||
"Failed to parse 'docker stack services' response from json",
|
||||
)?;
|
||||
|
||||
services.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
pub async fn list_swarm_stack_tasks(
|
||||
@@ -74,7 +109,7 @@ pub async fn list_swarm_stack_tasks(
|
||||
let res = run_komodo_standard_command(
|
||||
"List Swarm Stack Tasks",
|
||||
None,
|
||||
format!("docker stack ps --format json {stack}"),
|
||||
format!("docker stack ps --format json --no-trunc {stack}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -85,9 +120,53 @@ pub async fn list_swarm_stack_tasks(
|
||||
}
|
||||
|
||||
// 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")
|
||||
let mut tasks =
|
||||
serde_json::from_str::<Vec<SwarmStackTaskListItem>>(&format!(
|
||||
"[{}]",
|
||||
res.stdout.trim().replace('\n', ",")
|
||||
))
|
||||
.context(
|
||||
"Failed to parse 'docker stack ps' response from json",
|
||||
)?;
|
||||
|
||||
tasks.sort_by(|a, b| {
|
||||
a.desired_state
|
||||
.cmp(&b.desired_state)
|
||||
.then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
pub fn state_from_tasks<'a>(
|
||||
tasks: impl IntoIterator<Item = &'a SwarmStackTaskListItem>,
|
||||
) -> SwarmState {
|
||||
for task in tasks {
|
||||
let (Some(current), Some(desired)) =
|
||||
(&task.current_state, &task.desired_state)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// CurrentState example: 'Running 44 minutes ago'.
|
||||
// Only want first "word"
|
||||
let Some(current) = current.split(" ").next() else {
|
||||
continue;
|
||||
};
|
||||
if current != desired {
|
||||
return SwarmState::Unhealthy;
|
||||
}
|
||||
}
|
||||
SwarmState::Healthy
|
||||
}
|
||||
|
||||
fn cmp_option<T: Ord>(
|
||||
a: Option<T>,
|
||||
b: Option<T>,
|
||||
) -> std::cmp::Ordering {
|
||||
match (a, b) {
|
||||
(Some(a), Some(b)) => a.cmp(&b),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,19 @@ impl DockerClient {
|
||||
pub async fn list_swarm_tasks(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<SwarmTaskListItem>> {
|
||||
let tasks = self
|
||||
let mut tasks = self
|
||||
.docker
|
||||
.list_tasks(Option::<ListTasksOptions>::None)
|
||||
.await
|
||||
.context("Failed to query for swarm tasks list")?
|
||||
.into_iter()
|
||||
.map(convert_task_list_item)
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tasks.sort_by(|a, b| {
|
||||
a.state.cmp(&b.state).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ impl DockerClient {
|
||||
&self,
|
||||
containers: &[ContainerListItem],
|
||||
) -> anyhow::Result<Vec<VolumeListItem>> {
|
||||
let volumes = self
|
||||
let mut volumes = self
|
||||
.docker
|
||||
.list_volumes(Option::<ListVolumesOptions>::None)
|
||||
.await?
|
||||
@@ -45,7 +45,12 @@ impl DockerClient {
|
||||
in_use,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
volumes.sort_by(|a, b| {
|
||||
a.in_use.cmp(&b.in_use).then_with(|| a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
Ok(volumes)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::entities::{
|
||||
node::{SwarmNode, SwarmNodeListItem},
|
||||
secret::{SwarmSecret, SwarmSecretListItem},
|
||||
service::{SwarmService, SwarmServiceListItem},
|
||||
stack::{SwarmStackListItem, SwarmStackLists},
|
||||
stack::{SwarmStack, SwarmStackListItem},
|
||||
swarm::SwarmInspectInfo,
|
||||
task::{SwarmTask, SwarmTaskListItem},
|
||||
},
|
||||
@@ -484,4 +484,4 @@ pub struct InspectSwarmStack {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type InspectSwarmStackResponse = SwarmStackLists;
|
||||
pub type InspectSwarmStackResponse = SwarmStack;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::swarm::SwarmState;
|
||||
|
||||
/// Swarm stack list item.
|
||||
/// Returned by `docker stack ls --format json`
|
||||
///
|
||||
@@ -14,6 +16,14 @@ pub struct SwarmStackListItem {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Swarm stack state.
|
||||
/// - Healthy if all associated tasks match their desired state
|
||||
/// - Unhealthy otherwise
|
||||
///
|
||||
/// Not included in docker cli return, computed by Komodo
|
||||
#[serde(rename = "State")]
|
||||
pub state: Option<SwarmState>,
|
||||
|
||||
/// Number of services which are part of the stack
|
||||
#[serde(rename = "Services")]
|
||||
pub services: Option<String>,
|
||||
@@ -37,11 +47,19 @@ pub struct SwarmStackListItem {
|
||||
#[derive(
|
||||
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct SwarmStackLists {
|
||||
pub struct SwarmStack {
|
||||
/// Swarm stack name.
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
|
||||
/// Swarm stack state.
|
||||
/// - Healthy if all associated tasks match their desired state (or report no desired state)
|
||||
/// - Unhealthy otherwise
|
||||
///
|
||||
/// Not included in docker cli return, computed by Komodo
|
||||
#[serde(rename = "State")]
|
||||
pub state: SwarmState,
|
||||
|
||||
/// Services part of the stack
|
||||
#[serde(rename = "Services")]
|
||||
pub services: Vec<SwarmStackServiceListItem>,
|
||||
|
||||
@@ -29,16 +29,26 @@ pub struct SwarmListItemInfo {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Display,
|
||||
)]
|
||||
pub enum SwarmState {
|
||||
/// Unknown case
|
||||
#[default]
|
||||
Unknown,
|
||||
/// The Swarm is healthy, all nodes OK
|
||||
Healthy,
|
||||
/// The Swarm is unhealthy
|
||||
Unhealthy,
|
||||
/// Unknown case
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -4344,6 +4344,15 @@ export interface SwarmService {
|
||||
|
||||
export type InspectSwarmServiceResponse = SwarmService;
|
||||
|
||||
export enum SwarmState {
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy",
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
/**
|
||||
* Swarm stack service list item.
|
||||
* Returned by `docker stack services --format json <NAME>`
|
||||
@@ -4394,16 +4403,24 @@ export interface SwarmStackTaskListItem {
|
||||
* docker stack ps --format json <STACK>
|
||||
* ```
|
||||
*/
|
||||
export interface SwarmStackLists {
|
||||
export interface SwarmStack {
|
||||
/** Swarm stack name. */
|
||||
Name: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state (or report no desired state)
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State: SwarmState;
|
||||
/** Services part of the stack */
|
||||
Services: SwarmStackServiceListItem[];
|
||||
/** Tasks part of the stack */
|
||||
Tasks: SwarmStackTaskListItem[];
|
||||
}
|
||||
|
||||
export type InspectSwarmStackResponse = SwarmStackLists;
|
||||
export type InspectSwarmStackResponse = SwarmStack;
|
||||
|
||||
export enum TaskState {
|
||||
NEW = "new",
|
||||
@@ -5131,6 +5148,14 @@ export type ListSwarmServicesResponse = SwarmServiceListItem[];
|
||||
export interface SwarmStackListItem {
|
||||
/** Swarm stack name. */
|
||||
Name?: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State?: SwarmState;
|
||||
/** Number of services which are part of the stack */
|
||||
Services?: string;
|
||||
/** The stack orchestrator */
|
||||
@@ -5161,15 +5186,6 @@ export interface SwarmTaskListItem {
|
||||
|
||||
export type ListSwarmTasksResponse = SwarmTaskListItem[];
|
||||
|
||||
export enum SwarmState {
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown",
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy",
|
||||
}
|
||||
|
||||
export interface SwarmListItemInfo {
|
||||
/** Servers part of the swarm */
|
||||
server_ids: string[];
|
||||
|
||||
@@ -8,7 +8,7 @@ use komodo_client::entities::{
|
||||
node::{NodeSpecAvailabilityEnum, NodeSpecRoleEnum, SwarmNode},
|
||||
secret::SwarmSecret,
|
||||
service::SwarmService,
|
||||
stack::SwarmStackLists,
|
||||
stack::SwarmStack,
|
||||
swarm::SwarmInspectInfo,
|
||||
task::SwarmTask,
|
||||
},
|
||||
@@ -74,7 +74,7 @@ pub struct UpdateSwarmNode {
|
||||
// =======
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
|
||||
#[response(SwarmStackLists)]
|
||||
#[response(SwarmStack)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct InspectSwarmStack {
|
||||
/// The swarm stack name
|
||||
|
||||
36
frontend/public/client/types.d.ts
vendored
36
frontend/public/client/types.d.ts
vendored
@@ -4309,6 +4309,14 @@ export interface SwarmService {
|
||||
JobStatus?: ServiceJobStatus;
|
||||
}
|
||||
export type InspectSwarmServiceResponse = SwarmService;
|
||||
export declare enum SwarmState {
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy",
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown"
|
||||
}
|
||||
/**
|
||||
* Swarm stack service list item.
|
||||
* Returned by `docker stack services --format json <NAME>`
|
||||
@@ -4357,15 +4365,23 @@ export interface SwarmStackTaskListItem {
|
||||
* docker stack ps --format json <STACK>
|
||||
* ```
|
||||
*/
|
||||
export interface SwarmStackLists {
|
||||
export interface SwarmStack {
|
||||
/** Swarm stack name. */
|
||||
Name: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state (or report no desired state)
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State: SwarmState;
|
||||
/** Services part of the stack */
|
||||
Services: SwarmStackServiceListItem[];
|
||||
/** Tasks part of the stack */
|
||||
Tasks: SwarmStackTaskListItem[];
|
||||
}
|
||||
export type InspectSwarmStackResponse = SwarmStackLists;
|
||||
export type InspectSwarmStackResponse = SwarmStack;
|
||||
export declare enum TaskState {
|
||||
NEW = "new",
|
||||
ALLOCATED = "allocated",
|
||||
@@ -5005,6 +5021,14 @@ export type ListSwarmServicesResponse = SwarmServiceListItem[];
|
||||
export interface SwarmStackListItem {
|
||||
/** Swarm stack name. */
|
||||
Name?: string;
|
||||
/**
|
||||
* Swarm stack state.
|
||||
* - Healthy if all associated tasks match their desired state
|
||||
* - Unhealthy otherwise
|
||||
*
|
||||
* Not included in docker cli return, computed by Komodo
|
||||
*/
|
||||
State?: SwarmState;
|
||||
/** Number of services which are part of the stack */
|
||||
Services?: string;
|
||||
/** The stack orchestrator */
|
||||
@@ -5031,14 +5055,6 @@ export interface SwarmTaskListItem {
|
||||
UpdatedAt?: string;
|
||||
}
|
||||
export type ListSwarmTasksResponse = SwarmTaskListItem[];
|
||||
export declare enum SwarmState {
|
||||
/** Unknown case */
|
||||
Unknown = "Unknown",
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
Healthy = "Healthy",
|
||||
/** The Swarm is unhealthy */
|
||||
Unhealthy = "Unhealthy"
|
||||
}
|
||||
export interface SwarmListItemInfo {
|
||||
/** Servers part of the swarm */
|
||||
server_ids: string[];
|
||||
|
||||
@@ -611,6 +611,15 @@ export var ServiceUpdateStatusStateEnum;
|
||||
ServiceUpdateStatusStateEnum["ROLLBACK_PAUSED"] = "rollback_paused";
|
||||
ServiceUpdateStatusStateEnum["ROLLBACK_COMPLETED"] = "rollback_completed";
|
||||
})(ServiceUpdateStatusStateEnum || (ServiceUpdateStatusStateEnum = {}));
|
||||
export var SwarmState;
|
||||
(function (SwarmState) {
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
SwarmState["Healthy"] = "Healthy";
|
||||
/** The Swarm is unhealthy */
|
||||
SwarmState["Unhealthy"] = "Unhealthy";
|
||||
/** Unknown case */
|
||||
SwarmState["Unknown"] = "Unknown";
|
||||
})(SwarmState || (SwarmState = {}));
|
||||
export var TaskState;
|
||||
(function (TaskState) {
|
||||
TaskState["NEW"] = "new";
|
||||
@@ -709,15 +718,6 @@ export var StackState;
|
||||
/** Server not reachable for status */
|
||||
StackState["Unknown"] = "unknown";
|
||||
})(StackState || (StackState = {}));
|
||||
export var SwarmState;
|
||||
(function (SwarmState) {
|
||||
/** Unknown case */
|
||||
SwarmState["Unknown"] = "Unknown";
|
||||
/** The Swarm is healthy, all nodes OK */
|
||||
SwarmState["Healthy"] = "Healthy";
|
||||
/** The Swarm is unhealthy */
|
||||
SwarmState["Unhealthy"] = "Unhealthy";
|
||||
})(SwarmState || (SwarmState = {}));
|
||||
/**
|
||||
* Configures the behavior of [CreateTerminal] if the
|
||||
* specified terminal name already exists.
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
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 "..";
|
||||
import { SwarmServicesTable } from "../table";
|
||||
|
||||
export const SwarmServices = ({
|
||||
id,
|
||||
@@ -16,86 +11,16 @@ export const SwarmServices = ({
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const services =
|
||||
useRead("ListSwarmServices", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
|
||||
const filtered = filterBySplit(
|
||||
services,
|
||||
search,
|
||||
(service) => service.Name ?? service.ID ?? "Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
<SwarmServicesTable
|
||||
id={id}
|
||||
services={services}
|
||||
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-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,6 +69,12 @@ export const SwarmStacks = ({
|
||||
<SortableHeader column={column} title="Services" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
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 "..";
|
||||
import { SwarmTasksTable } from "../table";
|
||||
|
||||
export const SwarmTasks = ({
|
||||
id,
|
||||
@@ -16,134 +11,16 @@ export const SwarmTasks = ({
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const nodes =
|
||||
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const services =
|
||||
useRead("ListSwarmServices", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const _tasks =
|
||||
const tasks =
|
||||
useRead("ListSwarmTasks", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
|
||||
const tasks = _tasks.map((task) => {
|
||||
return {
|
||||
...task,
|
||||
node: nodes.find((node) => task.NodeID === node.ID),
|
||||
service: services.find((service) => task.ServiceID === service.ID),
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = filterBySplit(
|
||||
tasks,
|
||||
search,
|
||||
(task) => task.Name ?? task.service?.Name ?? "Unknown"
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
<SwarmTasksTable
|
||||
id={id}
|
||||
tasks={tasks}
|
||||
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-tasks"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Task"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.ID}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "service.Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Service" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.service?.ID}
|
||||
name={row.original.service?.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "node.Hostname",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Node" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Node"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.node?.ID}
|
||||
name={row.original.node?.Hostname}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "DesiredState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Desired State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { ResourceLink } from "../common";
|
||||
import { TableTags } from "@components/tags";
|
||||
import { SwarmComponents } from ".";
|
||||
import { SwarmComponents, SwarmLink } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import { useSelectedResources } from "@lib/hooks";
|
||||
import { useRead, useSelectedResources } from "@lib/hooks";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { Section } from "@components/layouts";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@ui/input";
|
||||
|
||||
export const SwarmTable = ({ swarms }: { swarms: Types.SwarmListItem[] }) => {
|
||||
const [_, setSelectedResources] = useSelectedResources("Swarm");
|
||||
@@ -41,3 +46,437 @@ export const SwarmTable = ({ swarms }: { swarms: Types.SwarmListItem[] }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmServicesTable = ({
|
||||
id,
|
||||
services,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
services: Types.SwarmServiceListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const filtered = filterBySplit(
|
||||
services,
|
||||
search,
|
||||
(service) => service.Name ?? service.ID ?? "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-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.Name}
|
||||
name={row.original.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmStackServicesTable = ({
|
||||
id,
|
||||
services,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
services: Types.SwarmStackServiceListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const filtered = filterBySplit(
|
||||
services,
|
||||
search,
|
||||
(service) => service.Name ?? service.ID ?? "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-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.Name}
|
||||
name={row.original.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Image",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Image" />
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Mode",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Mode" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "Replicas",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Replicas" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "Ports",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Ports" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmTasksTable = ({
|
||||
id,
|
||||
tasks: _tasks,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
tasks: Types.SwarmTaskListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
|
||||
const nodes =
|
||||
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const services =
|
||||
useRead("ListSwarmServices", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const tasks = _tasks.map((task) => {
|
||||
return {
|
||||
...task,
|
||||
node: nodes.find((node) => task.NodeID === node.ID),
|
||||
service: services.find((service) => task.ServiceID === service.ID),
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = filterBySplit(
|
||||
tasks,
|
||||
search,
|
||||
(task) =>
|
||||
task.Name ?? task.service?.Name ?? task.node?.Hostname ?? "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-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Task"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.ID}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "service.Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Service" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Service"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.service?.ID}
|
||||
name={row.original.service?.Name}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "node.Hostname",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Node" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Node"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.node?.ID}
|
||||
name={row.original.node?.Hostname}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "DesiredState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Desired State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Updated" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.UpdatedAt
|
||||
? new Date(row.original.UpdatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "CreatedAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.CreatedAt
|
||||
? new Date(row.original.CreatedAt).toLocaleString()
|
||||
: "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const SwarmStackTasksTable = ({
|
||||
id,
|
||||
tasks: _tasks,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
tasks: Types.SwarmStackTaskListItem[];
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
|
||||
const nodes =
|
||||
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
const tasks = _tasks.map((task) => {
|
||||
return {
|
||||
...task,
|
||||
node: nodes.find(
|
||||
(node) =>
|
||||
(task.Node ?? false) &&
|
||||
(task.Node === node.ID ||
|
||||
task.Node === node.Hostname ||
|
||||
task.Node === node.Name)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = filterBySplit(
|
||||
tasks,
|
||||
search,
|
||||
(task) => task.Name ?? task.node?.Hostname ?? "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-tasks"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Task"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.ID}
|
||||
name={row.original.ID}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "node.Hostname",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Node" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<SwarmLink
|
||||
type="Node"
|
||||
swarm_id={id}
|
||||
resource_id={row.original.node?.ID}
|
||||
name={row.original.node?.Hostname}
|
||||
/>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Image",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Image" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "CurrentState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "DesiredState",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Desired State" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,7 +36,13 @@ export default function SwarmServicePage() {
|
||||
const { data: services, isPending } = useRead("ListSwarmServices", {
|
||||
swarm: id,
|
||||
});
|
||||
const service = services?.find((service) => service.ID === _service);
|
||||
const service = services?.find(
|
||||
(service) =>
|
||||
_service &&
|
||||
// First match on name here.
|
||||
// Then better to match on ID start to accept short ids too.
|
||||
(service.Name === _service || service.ID?.startsWith(_service))
|
||||
);
|
||||
const tasks =
|
||||
useRead("ListSwarmTasks", {
|
||||
swarm: id,
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import {
|
||||
ResourceDescription,
|
||||
ResourceLink,
|
||||
ResourcePageHeader,
|
||||
} from "@components/resources/common";
|
||||
import {
|
||||
useLocalStorage,
|
||||
usePermissions,
|
||||
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 { Link, useParams } from "react-router-dom";
|
||||
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
|
||||
import {
|
||||
stroke_color_class_by_intention,
|
||||
swarm_state_intention,
|
||||
} from "@lib/color";
|
||||
import { ExportButton } from "@components/export";
|
||||
import { ResourceNotifications } from "@pages/resource-notifications";
|
||||
import { Types } from "komodo_client";
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { MobileFriendlyTabsSelector } from "@ui/mobile-friendly-tabs";
|
||||
import { Section } from "@components/layouts";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import {
|
||||
SwarmStackServicesTable,
|
||||
SwarmStackTasksTable,
|
||||
} from "@components/resources/swarm/table";
|
||||
|
||||
export default function SwarmStackPage() {
|
||||
const { id, stack: __stack } = useParams() as {
|
||||
@@ -13,14 +35,16 @@ export default function SwarmStackPage() {
|
||||
stack: string;
|
||||
};
|
||||
const _stack = decodeURIComponent(__stack);
|
||||
console.log(_stack);
|
||||
const swarm = useSwarm(id);
|
||||
const { data: stack, isPending } = useRead("InspectSwarmStack", {
|
||||
swarm: id,
|
||||
stack: _stack,
|
||||
});
|
||||
const { canWrite } = usePermissions({
|
||||
type: "Swarm",
|
||||
id,
|
||||
});
|
||||
useSetTitle(`${swarm?.name} | Stack | ${stack?.Name ?? "Unknown"}`);
|
||||
const nav = useNavigate();
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
@@ -35,42 +59,147 @@ export default function SwarmStackPage() {
|
||||
}
|
||||
|
||||
const Icon = SWARM_ICONS.Stack;
|
||||
const state = stack.State;
|
||||
const intention = swarm_state_intention(state);
|
||||
const strokeColor = stroke_color_class_by_intention(intention);
|
||||
|
||||
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
|
||||
<div>
|
||||
<div className="w-full flex items-center justify-between mb-12">
|
||||
<Link to={"/swarms/" + id}>
|
||||
<Button className="gap-2" variant="secondary">
|
||||
<ChevronLeft className="w-4" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* TITLE */}
|
||||
</Link>
|
||||
<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} />
|
||||
<ExportButton targets={[{ type: "Swarm", id }]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col xl:flex-row gap-4">
|
||||
{/* HEADER */}
|
||||
<div className="w-full flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2 border rounded-md">
|
||||
<ResourcePageHeader
|
||||
type={undefined}
|
||||
id={undefined}
|
||||
intent={intention}
|
||||
icon={<Icon size={8} className={strokeColor} />}
|
||||
resource={undefined}
|
||||
name={stack.Name}
|
||||
state={state}
|
||||
status={`${stack.Services.length} Services`}
|
||||
/>
|
||||
<div className="flex flex-col pb-2 px-4">
|
||||
<div className="flex items-center gap-x-4 gap-y-0 flex-wrap text-muted-foreground">
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
<div>|</div>
|
||||
<div>Swarm Stack</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ResourceDescription type="Swarm" id={id} disabled={!canWrite} />
|
||||
</div>
|
||||
{/** NOTIFICATIONS */}
|
||||
<ResourceNotifications type="Swarm" id={id} />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-12">
|
||||
{/* Actions */}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="pt-4">
|
||||
{swarm && <SwarmStackTabs swarm={swarm} stack={stack} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SwarmStackTabsView = "Services" | "Tasks" | "Inspect";
|
||||
|
||||
const SwarmStackTabs = ({
|
||||
swarm,
|
||||
stack,
|
||||
}: {
|
||||
swarm: Types.SwarmListItem;
|
||||
stack: Types.SwarmStack;
|
||||
}) => {
|
||||
const [_view, setView] = useLocalStorage<SwarmStackTabsView>(
|
||||
`swarm-${swarm.id}-stack-${stack}-tabs-v2`,
|
||||
"Services"
|
||||
);
|
||||
const _search = useState("");
|
||||
const { specificInspect } = usePermissions({
|
||||
type: "Swarm",
|
||||
id: swarm.id,
|
||||
});
|
||||
|
||||
const view = !specificInspect && _view === "Inspect" ? "Log" : _view;
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: "Services",
|
||||
},
|
||||
{
|
||||
value: "Tasks",
|
||||
},
|
||||
{
|
||||
value: "Inspect",
|
||||
disabled: !specificInspect,
|
||||
},
|
||||
],
|
||||
[specificInspect]
|
||||
);
|
||||
|
||||
const Selector = (
|
||||
<MobileFriendlyTabsSelector
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabsTriggerClassname="w-[110px]"
|
||||
/>
|
||||
);
|
||||
|
||||
switch (view) {
|
||||
case "Services":
|
||||
return (
|
||||
<SwarmStackServicesTable
|
||||
id={swarm.id}
|
||||
services={stack.Services}
|
||||
titleOther={Selector}
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
case "Tasks":
|
||||
return (
|
||||
<SwarmStackTasksTable
|
||||
id={swarm.id}
|
||||
tasks={stack.Tasks}
|
||||
titleOther={Selector}
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
case "Inspect":
|
||||
return <SwarmStackInspect stack={stack} titleOther={Selector} />;
|
||||
}
|
||||
};
|
||||
|
||||
const SwarmStackInspect = ({
|
||||
stack,
|
||||
titleOther,
|
||||
}: {
|
||||
stack: Types.SwarmStack;
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(stack, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,7 +36,12 @@ export default function SwarmTaskPage() {
|
||||
const { data: tasks, isPending } = useRead("ListSwarmTasks", {
|
||||
swarm: id,
|
||||
});
|
||||
const task = tasks?.find((task) => task.ID === _task);
|
||||
const task = tasks?.find(
|
||||
(task) =>
|
||||
_task &&
|
||||
// Better to match on start to accept short ids too
|
||||
task.ID?.startsWith(_task)
|
||||
);
|
||||
const node = useRead("ListSwarmNodes", { swarm: id }).data?.find(
|
||||
(node) => node.ID === task?.NodeID
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user