mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
basic swarm
This commit is contained in:
@@ -81,7 +81,12 @@ enum ReadRequest {
|
||||
GetSwarm(GetSwarm),
|
||||
GetSwarmActionState(GetSwarmActionState),
|
||||
ListSwarms(ListSwarms),
|
||||
InspectSwarm(InspectSwarm),
|
||||
ListFullSwarms(ListFullSwarms),
|
||||
ListSwarmNodes(ListSwarmNodes),
|
||||
ListSwarmServices(ListSwarmServices),
|
||||
ListSwarmTasks(ListSwarmTasks),
|
||||
ListSwarmSecrets(ListSwarmSecrets),
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary(GetServersSummary),
|
||||
|
||||
@@ -9,8 +9,10 @@ use komodo_client::{
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, permission::get_check_permissions,
|
||||
resource, state::{action_states, swarm_status_cache},
|
||||
helpers::query::get_all_tags,
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, swarm_status_cache},
|
||||
};
|
||||
|
||||
use super::ReadArgs;
|
||||
@@ -138,3 +140,109 @@ impl Resolve<ReadArgs> for GetSwarmsSummary {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for InspectSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmResponse> {
|
||||
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;
|
||||
let inspect = cache
|
||||
.inspect
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.context("SwarmInspectInfo not available")?;
|
||||
Ok(inspect)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmNodes {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmNodesResponse> {
|
||||
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.nodes.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmServices {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmServicesResponse> {
|
||||
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.services.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmTasks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmTasksResponse> {
|
||||
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.tasks.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListSwarmSecrets {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmSecretsResponse> {
|
||||
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.secrets.clone())
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use anyhow::anyhow;
|
||||
use async_timing_util::wait_until_timelength;
|
||||
use cache::CloneCache;
|
||||
use database::mungos::find::find_collect;
|
||||
use formatting::format_serror;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::entities::{
|
||||
docker::node::NodeState,
|
||||
@@ -100,9 +101,9 @@ pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) {
|
||||
state: SwarmState::Unknown,
|
||||
inspect: None,
|
||||
lists: None,
|
||||
err: Some(
|
||||
anyhow!("No Servers configured as manager nodes").into(),
|
||||
),
|
||||
err: Some(format_serror(
|
||||
&anyhow!("No Servers configured as manager nodes").into(),
|
||||
)),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -121,7 +122,7 @@ pub async fn update_cache_for_swarm(swarm: &Swarm, force: bool) {
|
||||
state: SwarmState::Unknown,
|
||||
inspect: None,
|
||||
lists: None,
|
||||
err: Some(e.into()),
|
||||
err: Some(format_serror(&e.into())),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
|
||||
@@ -45,10 +45,10 @@ impl super::KomodoResource for Swarm {
|
||||
async fn to_list_item(
|
||||
swarm: Resource<Self::Config, Self::Info>,
|
||||
) -> Self::ListItem {
|
||||
let state = swarm_status_cache()
|
||||
let (state, err) = swarm_status_cache()
|
||||
.get(&swarm.id)
|
||||
.await
|
||||
.map(|status| status.state)
|
||||
.map(|status| (status.state, status.err.clone()))
|
||||
.unwrap_or_default();
|
||||
SwarmListItem {
|
||||
name: swarm.name,
|
||||
@@ -59,6 +59,7 @@ impl super::KomodoResource for Swarm {
|
||||
info: SwarmListItemInfo {
|
||||
server_ids: swarm.config.server_ids,
|
||||
state,
|
||||
err,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ pub struct CachedSwarmStatus {
|
||||
pub inspect: Option<SwarmInspectInfo>,
|
||||
pub lists: Option<SwarmLists>,
|
||||
/// Store the error in communicating with Swarm
|
||||
pub err: Option<serror::Serror>,
|
||||
pub err: Option<String>,
|
||||
}
|
||||
|
||||
pub type SwarmStatusCache =
|
||||
|
||||
@@ -137,7 +137,7 @@ pub enum PeripheryRequest {
|
||||
PruneSystem(PruneSystem),
|
||||
|
||||
// Swarm
|
||||
GetSwarmLists(PollSwarmStatus),
|
||||
PollSwarmStatus(PollSwarmStatus),
|
||||
InspectSwarmNode(InspectSwarmNode),
|
||||
InspectSwarmService(InspectSwarmService),
|
||||
InspectSwarmTask(InspectSwarmTask),
|
||||
|
||||
@@ -3,8 +3,12 @@ use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
use crate::entities::swarm::{
|
||||
Swarm, SwarmActionState, SwarmListItem, SwarmQuery,
|
||||
use crate::entities::{
|
||||
docker::{
|
||||
node::SwarmNode, secret::SwarmSecret, service::SwarmService,
|
||||
swarm::SwarmInspectInfo, task::SwarmTask,
|
||||
},
|
||||
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmQuery},
|
||||
};
|
||||
|
||||
use super::KomodoReadRequest;
|
||||
@@ -111,3 +115,103 @@ pub struct GetSwarmsSummaryResponse {
|
||||
/// The number of Swarms with Unknown state
|
||||
pub unknown: u32,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// Inspect information about the swarm.
|
||||
/// Response: [SwarmInspectInfo].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(InspectSwarmResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct InspectSwarm {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub swarm: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type InspectSwarmResponse = SwarmInspectInfo;
|
||||
|
||||
//
|
||||
|
||||
/// List nodes part of the target Swarm.
|
||||
/// Response: [ListSwarmNodesResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListSwarmNodesResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListSwarmNodes {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub swarm: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListSwarmNodesResponse = Vec<SwarmNode>;
|
||||
|
||||
//
|
||||
|
||||
/// List services on the target Swarm.
|
||||
/// Response: [ListSwarmServicesResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListSwarmServicesResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListSwarmServices {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub swarm: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListSwarmServicesResponse = Vec<SwarmService>;
|
||||
|
||||
//
|
||||
|
||||
/// List tasks on the target Swarm.
|
||||
/// Response: [ListSwarmTasksResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListSwarmTasksResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListSwarmTasks {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub swarm: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListSwarmTasksResponse = Vec<SwarmTask>;
|
||||
|
||||
//
|
||||
|
||||
/// List secrets on the target Swarm.
|
||||
/// Response: [ListSwarmSecretsResponse].
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
|
||||
)]
|
||||
#[empty_traits(KomodoReadRequest)]
|
||||
#[response(ListSwarmSecretsResponse)]
|
||||
#[error(serror::Error)]
|
||||
pub struct ListSwarmSecrets {
|
||||
/// Id or name
|
||||
#[serde(alias = "id", alias = "name")]
|
||||
pub swarm: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type ListSwarmSecretsResponse = Vec<SwarmSecret>;
|
||||
|
||||
@@ -22,6 +22,9 @@ pub struct SwarmListItemInfo {
|
||||
pub server_ids: Vec<String>,
|
||||
/// The Swarm state
|
||||
pub state: SwarmState,
|
||||
/// If there is an error reaching
|
||||
/// Swarm, message will be given here.
|
||||
pub err: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -28,6 +28,11 @@ export type ReadResponses = {
|
||||
GetSwarmActionState: Types.GetSwarmActionStateResponse;
|
||||
ListSwarms: Types.ListSwarmsResponse;
|
||||
ListFullSwarms: Types.ListFullSwarmsResponse;
|
||||
InspectSwarm: Types.InspectSwarmResponse;
|
||||
ListSwarmNodes: Types.ListSwarmNodesResponse;
|
||||
ListSwarmServices: Types.ListSwarmServicesResponse;
|
||||
ListSwarmTasks: Types.ListSwarmTasksResponse;
|
||||
ListSwarmSecrets: Types.ListSwarmSecretsResponse;
|
||||
|
||||
// ==== SERVER ====
|
||||
GetServersSummary: Types.GetServersSummaryResponse;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
5
frontend/public/client/responses.d.ts
vendored
5
frontend/public/client/responses.d.ts
vendored
@@ -23,6 +23,11 @@ export type ReadResponses = {
|
||||
GetSwarmActionState: Types.GetSwarmActionStateResponse;
|
||||
ListSwarms: Types.ListSwarmsResponse;
|
||||
ListFullSwarms: Types.ListFullSwarmsResponse;
|
||||
InspectSwarm: Types.InspectSwarmResponse;
|
||||
ListSwarmNodes: Types.ListSwarmNodesResponse;
|
||||
ListSwarmServices: Types.ListSwarmServicesResponse;
|
||||
ListSwarmTasks: Types.ListSwarmTasksResponse;
|
||||
ListSwarmSecrets: Types.ListSwarmSecretsResponse;
|
||||
GetServersSummary: Types.GetServersSummaryResponse;
|
||||
GetServer: Types.GetServerResponse;
|
||||
GetServerState: Types.GetServerStateResponse;
|
||||
|
||||
1525
frontend/public/client/types.d.ts
vendored
1525
frontend/public/client/types.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -492,6 +492,11 @@ export var ClusterVolumePublishStatusStateEnum;
|
||||
ClusterVolumePublishStatusStateEnum["PendingNodeUnpublish"] = "pending-node-unpublish";
|
||||
ClusterVolumePublishStatusStateEnum["PendingControllerUnpublish"] = "pending-controller-unpublish";
|
||||
})(ClusterVolumePublishStatusStateEnum || (ClusterVolumePublishStatusStateEnum = {}));
|
||||
export var SwarmSpecCaConfigExternalCasProtocolEnum;
|
||||
(function (SwarmSpecCaConfigExternalCasProtocolEnum) {
|
||||
SwarmSpecCaConfigExternalCasProtocolEnum["EMPTY"] = "";
|
||||
SwarmSpecCaConfigExternalCasProtocolEnum["CFSSL"] = "cfssl";
|
||||
})(SwarmSpecCaConfigExternalCasProtocolEnum || (SwarmSpecCaConfigExternalCasProtocolEnum = {}));
|
||||
export var PortTypeEnum;
|
||||
(function (PortTypeEnum) {
|
||||
PortTypeEnum["EMPTY"] = "";
|
||||
@@ -572,63 +577,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.
|
||||
*/
|
||||
export var TerminalRecreateMode;
|
||||
(function (TerminalRecreateMode) {
|
||||
/**
|
||||
* Never kill the old terminal if it already exists.
|
||||
* If the init command is different, returns error.
|
||||
*/
|
||||
TerminalRecreateMode["Never"] = "Never";
|
||||
/** Always kill the old terminal and create new one */
|
||||
TerminalRecreateMode["Always"] = "Always";
|
||||
/** Only kill and recreate if the command is different. */
|
||||
TerminalRecreateMode["DifferentCommand"] = "DifferentCommand";
|
||||
})(TerminalRecreateMode || (TerminalRecreateMode = {}));
|
||||
/** Specify the container terminal mode (exec or attach) */
|
||||
export var ContainerTerminalMode;
|
||||
(function (ContainerTerminalMode) {
|
||||
ContainerTerminalMode["Exec"] = "exec";
|
||||
ContainerTerminalMode["Attach"] = "attach";
|
||||
})(ContainerTerminalMode || (ContainerTerminalMode = {}));
|
||||
export var EndpointPortConfigProtocolEnum;
|
||||
(function (EndpointPortConfigProtocolEnum) {
|
||||
EndpointPortConfigProtocolEnum["EMPTY"] = "";
|
||||
EndpointPortConfigProtocolEnum["TCP"] = "tcp";
|
||||
EndpointPortConfigProtocolEnum["UDP"] = "udp";
|
||||
EndpointPortConfigProtocolEnum["SCTP"] = "sctp";
|
||||
})(EndpointPortConfigProtocolEnum || (EndpointPortConfigProtocolEnum = {}));
|
||||
export var EndpointPortConfigPublishModeEnum;
|
||||
(function (EndpointPortConfigPublishModeEnum) {
|
||||
EndpointPortConfigPublishModeEnum["EMPTY"] = "";
|
||||
EndpointPortConfigPublishModeEnum["INGRESS"] = "ingress";
|
||||
EndpointPortConfigPublishModeEnum["HOST"] = "host";
|
||||
})(EndpointPortConfigPublishModeEnum || (EndpointPortConfigPublishModeEnum = {}));
|
||||
export var EndpointSpecModeEnum;
|
||||
(function (EndpointSpecModeEnum) {
|
||||
EndpointSpecModeEnum["EMPTY"] = "";
|
||||
EndpointSpecModeEnum["VIP"] = "vip";
|
||||
EndpointSpecModeEnum["DNSRR"] = "dnsrr";
|
||||
})(EndpointSpecModeEnum || (EndpointSpecModeEnum = {}));
|
||||
/** Reachability represents the reachability of a node. */
|
||||
export var NodeReachability;
|
||||
(function (NodeReachability) {
|
||||
NodeReachability["UNKNOWN"] = "unknown";
|
||||
NodeReachability["UNREACHABLE"] = "unreachable";
|
||||
NodeReachability["REACHABLE"] = "reachable";
|
||||
})(NodeReachability || (NodeReachability = {}));
|
||||
export var NodeSpecRoleEnum;
|
||||
(function (NodeSpecRoleEnum) {
|
||||
NodeSpecRoleEnum["EMPTY"] = "";
|
||||
@@ -650,26 +598,13 @@ export var NodeState;
|
||||
NodeState["READY"] = "ready";
|
||||
NodeState["DISCONNECTED"] = "disconnected";
|
||||
})(NodeState || (NodeState = {}));
|
||||
export var DefaultRepoFolder;
|
||||
(function (DefaultRepoFolder) {
|
||||
/** /${root_directory}/stacks */
|
||||
DefaultRepoFolder["Stacks"] = "Stacks";
|
||||
/** /${root_directory}/builds */
|
||||
DefaultRepoFolder["Builds"] = "Builds";
|
||||
/** /${root_directory}/repos */
|
||||
DefaultRepoFolder["Repos"] = "Repos";
|
||||
/**
|
||||
* If the repo is only cloned
|
||||
* in the core repo cache (resource sync),
|
||||
* this isn't relevant.
|
||||
*/
|
||||
DefaultRepoFolder["NotApplicable"] = "NotApplicable";
|
||||
})(DefaultRepoFolder || (DefaultRepoFolder = {}));
|
||||
export var SearchCombinator;
|
||||
(function (SearchCombinator) {
|
||||
SearchCombinator["Or"] = "Or";
|
||||
SearchCombinator["And"] = "And";
|
||||
})(SearchCombinator || (SearchCombinator = {}));
|
||||
/** Reachability represents the reachability of a node. */
|
||||
export var NodeReachability;
|
||||
(function (NodeReachability) {
|
||||
NodeReachability["UNKNOWN"] = "unknown";
|
||||
NodeReachability["UNREACHABLE"] = "unreachable";
|
||||
NodeReachability["REACHABLE"] = "reachable";
|
||||
})(NodeReachability || (NodeReachability = {}));
|
||||
export var TaskSpecContainerSpecPrivilegesSeccompModeEnum;
|
||||
(function (TaskSpecContainerSpecPrivilegesSeccompModeEnum) {
|
||||
TaskSpecContainerSpecPrivilegesSeccompModeEnum["EMPTY"] = "";
|
||||
@@ -722,6 +657,25 @@ export var ServiceSpecRollbackConfigOrderEnum;
|
||||
ServiceSpecRollbackConfigOrderEnum["STOP_FIRST"] = "stop-first";
|
||||
ServiceSpecRollbackConfigOrderEnum["START_FIRST"] = "start-first";
|
||||
})(ServiceSpecRollbackConfigOrderEnum || (ServiceSpecRollbackConfigOrderEnum = {}));
|
||||
export var EndpointSpecModeEnum;
|
||||
(function (EndpointSpecModeEnum) {
|
||||
EndpointSpecModeEnum["EMPTY"] = "";
|
||||
EndpointSpecModeEnum["VIP"] = "vip";
|
||||
EndpointSpecModeEnum["DNSRR"] = "dnsrr";
|
||||
})(EndpointSpecModeEnum || (EndpointSpecModeEnum = {}));
|
||||
export var EndpointPortConfigProtocolEnum;
|
||||
(function (EndpointPortConfigProtocolEnum) {
|
||||
EndpointPortConfigProtocolEnum["EMPTY"] = "";
|
||||
EndpointPortConfigProtocolEnum["TCP"] = "tcp";
|
||||
EndpointPortConfigProtocolEnum["UDP"] = "udp";
|
||||
EndpointPortConfigProtocolEnum["SCTP"] = "sctp";
|
||||
})(EndpointPortConfigProtocolEnum || (EndpointPortConfigProtocolEnum = {}));
|
||||
export var EndpointPortConfigPublishModeEnum;
|
||||
(function (EndpointPortConfigPublishModeEnum) {
|
||||
EndpointPortConfigPublishModeEnum["EMPTY"] = "";
|
||||
EndpointPortConfigPublishModeEnum["INGRESS"] = "ingress";
|
||||
EndpointPortConfigPublishModeEnum["HOST"] = "host";
|
||||
})(EndpointPortConfigPublishModeEnum || (EndpointPortConfigPublishModeEnum = {}));
|
||||
export var ServiceUpdateStatusStateEnum;
|
||||
(function (ServiceUpdateStatusStateEnum) {
|
||||
ServiceUpdateStatusStateEnum["EMPTY"] = "";
|
||||
@@ -732,11 +686,6 @@ export var ServiceUpdateStatusStateEnum;
|
||||
ServiceUpdateStatusStateEnum["ROLLBACK_PAUSED"] = "rollback_paused";
|
||||
ServiceUpdateStatusStateEnum["ROLLBACK_COMPLETED"] = "rollback_completed";
|
||||
})(ServiceUpdateStatusStateEnum || (ServiceUpdateStatusStateEnum = {}));
|
||||
export var SwarmSpecCaConfigExternalCasProtocolEnum;
|
||||
(function (SwarmSpecCaConfigExternalCasProtocolEnum) {
|
||||
SwarmSpecCaConfigExternalCasProtocolEnum["EMPTY"] = "";
|
||||
SwarmSpecCaConfigExternalCasProtocolEnum["CFSSL"] = "cfssl";
|
||||
})(SwarmSpecCaConfigExternalCasProtocolEnum || (SwarmSpecCaConfigExternalCasProtocolEnum = {}));
|
||||
export var TaskState;
|
||||
(function (TaskState) {
|
||||
TaskState["NEW"] = "new";
|
||||
@@ -755,6 +704,57 @@ export var TaskState;
|
||||
TaskState["REMOVE"] = "remove";
|
||||
TaskState["ORPHANED"] = "orphaned";
|
||||
})(TaskState || (TaskState = {}));
|
||||
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.
|
||||
*/
|
||||
export var TerminalRecreateMode;
|
||||
(function (TerminalRecreateMode) {
|
||||
/**
|
||||
* Never kill the old terminal if it already exists.
|
||||
* If the init command is different, returns error.
|
||||
*/
|
||||
TerminalRecreateMode["Never"] = "Never";
|
||||
/** Always kill the old terminal and create new one */
|
||||
TerminalRecreateMode["Always"] = "Always";
|
||||
/** Only kill and recreate if the command is different. */
|
||||
TerminalRecreateMode["DifferentCommand"] = "DifferentCommand";
|
||||
})(TerminalRecreateMode || (TerminalRecreateMode = {}));
|
||||
/** Specify the container terminal mode (exec or attach) */
|
||||
export var ContainerTerminalMode;
|
||||
(function (ContainerTerminalMode) {
|
||||
ContainerTerminalMode["Exec"] = "exec";
|
||||
ContainerTerminalMode["Attach"] = "attach";
|
||||
})(ContainerTerminalMode || (ContainerTerminalMode = {}));
|
||||
export var DefaultRepoFolder;
|
||||
(function (DefaultRepoFolder) {
|
||||
/** /${root_directory}/stacks */
|
||||
DefaultRepoFolder["Stacks"] = "Stacks";
|
||||
/** /${root_directory}/builds */
|
||||
DefaultRepoFolder["Builds"] = "Builds";
|
||||
/** /${root_directory}/repos */
|
||||
DefaultRepoFolder["Repos"] = "Repos";
|
||||
/**
|
||||
* If the repo is only cloned
|
||||
* in the core repo cache (resource sync),
|
||||
* this isn't relevant.
|
||||
*/
|
||||
DefaultRepoFolder["NotApplicable"] = "NotApplicable";
|
||||
})(DefaultRepoFolder || (DefaultRepoFolder = {}));
|
||||
export var SearchCombinator;
|
||||
(function (SearchCombinator) {
|
||||
SearchCombinator["Or"] = "Or";
|
||||
SearchCombinator["And"] = "And";
|
||||
})(SearchCombinator || (SearchCombinator = {}));
|
||||
/** Days of the week */
|
||||
export var DayOfWeek;
|
||||
(function (DayOfWeek) {
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Config } from "@components/config";
|
||||
import { ConfigItem } from "@components/config/util";
|
||||
import { ConfigItem, ConfigList } from "@components/config/util";
|
||||
import { useLocalStorage, usePermissions, useRead, useWrite } from "@lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { ResourceSelector } from "../common";
|
||||
import { Button } from "@ui/button";
|
||||
import { MinusCircle, PlusCircle } from "lucide-react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const SwarmConfig = ({ id }: { id: string }) => {
|
||||
export const SwarmConfig = ({
|
||||
id,
|
||||
titleOther,
|
||||
}: {
|
||||
id: string;
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
const { canWrite } = usePermissions({ type: "Swarm", id });
|
||||
const swarm = useRead("GetSwarm", { swarm: id }).data;
|
||||
const config = swarm?.config;
|
||||
@@ -24,6 +31,7 @@ export const SwarmConfig = ({ id }: { id: string }) => {
|
||||
|
||||
return (
|
||||
<Config
|
||||
titleOther={titleOther}
|
||||
disabled={disabled}
|
||||
original={config}
|
||||
update={update}
|
||||
@@ -41,6 +49,7 @@ export const SwarmConfig = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<ConfigItem
|
||||
label="Manager Nodes"
|
||||
boldLabel
|
||||
description="Select the Servers which have joined the Swarm as Manager Nodes."
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
@@ -100,6 +109,25 @@ export const SwarmConfig = ({ id }: { id: string }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Links",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
links: (values, set) => (
|
||||
<ConfigList
|
||||
label="Links"
|
||||
boldLabel
|
||||
addLabel="Add Link"
|
||||
description="Add quick links in the resource header"
|
||||
field="links"
|
||||
values={values ?? []}
|
||||
set={set}
|
||||
disabled={disabled}
|
||||
placeholder="Input link"
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Boxes } from "lucide-react";
|
||||
import { SwarmConfig } from "./config";
|
||||
import { DeleteResource, NewResource, ResourcePageHeader } from "../common";
|
||||
import { SwarmTable } from "./table";
|
||||
import {
|
||||
swarm_state_intention,
|
||||
stroke_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { cn } from "@lib/utils";
|
||||
import { cn, updateLogToHtml } from "@lib/utils";
|
||||
import { Types } from "komodo_client";
|
||||
import { DashboardPieChart } from "@components/util";
|
||||
import { StatusBadge } from "@components/util";
|
||||
import { GroupActions } from "@components/group-actions";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
|
||||
import { Card } from "@ui/card";
|
||||
import { SwarmTabs } from "./tabs";
|
||||
|
||||
export const useSwarm = (id?: string) =>
|
||||
useRead("ListSwarms", {}, { refetchInterval: 10_000 }).data?.find(
|
||||
@@ -73,13 +75,36 @@ export const SwarmComponents: RequiredResourceComponents = {
|
||||
|
||||
Info: {},
|
||||
|
||||
Status: {},
|
||||
Status: {
|
||||
Err: ({ id }) => {
|
||||
const err = useSwarm(id)?.info.err;
|
||||
if (!err) return null;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Card className="px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer">
|
||||
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
|
||||
Error
|
||||
</div>
|
||||
</Card>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="w-fit max-w-[90vw] md:max-w-[60vw]">
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: updateLogToHtml(err),
|
||||
}}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
Actions: {},
|
||||
|
||||
Page: {},
|
||||
|
||||
Config: SwarmConfig,
|
||||
Config: SwarmTabs,
|
||||
|
||||
DangerZone: ({ id }) => <DeleteResource type="Swarm" id={id} />,
|
||||
|
||||
|
||||
88
frontend/src/components/resources/swarm/info/index.tsx
Normal file
88
frontend/src/components/resources/swarm/info/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { useSwarm } from "..";
|
||||
import { Types } from "komodo_client";
|
||||
import { useLocalStorage } from "@lib/hooks";
|
||||
import {
|
||||
MobileFriendlyTabsSelector,
|
||||
TabNoContent,
|
||||
} from "@ui/mobile-friendly-tabs";
|
||||
import { SwarmNodes } from "./nodes";
|
||||
import { SwarmSecrets } from "./secrets";
|
||||
import { SwarmServices } from "./services";
|
||||
import { SwarmTasks } from "./tasks";
|
||||
|
||||
type SwarmInfoView = "Nodes" | "Services" | "Tasks" | "Secrets";
|
||||
|
||||
export const SwarmInfo = ({
|
||||
id,
|
||||
titleOther,
|
||||
}: {
|
||||
id: string;
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
const _search = useState("");
|
||||
const state = useSwarm(id)?.info.state ?? Types.SwarmState.Unknown;
|
||||
const [view, setView] = useLocalStorage<SwarmInfoView>(
|
||||
"swarm-info-view-v1",
|
||||
"Nodes"
|
||||
);
|
||||
|
||||
if (state === Types.SwarmState.Unknown) {
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
<h2 className="text-muted-foreground">
|
||||
Swarm unreachable, info is not available
|
||||
</h2>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
const tabsNoContent = useMemo<TabNoContent<SwarmInfoView>[]>(
|
||||
() => [
|
||||
{
|
||||
value: "Nodes",
|
||||
},
|
||||
{
|
||||
value: "Services",
|
||||
},
|
||||
{
|
||||
value: "Tasks",
|
||||
},
|
||||
{
|
||||
value: "Secrets",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const Selector = (
|
||||
<MobileFriendlyTabsSelector
|
||||
tabs={tabsNoContent}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabsTriggerClassname="w-[110px]"
|
||||
/>
|
||||
);
|
||||
|
||||
const Component = () => {
|
||||
switch (view) {
|
||||
case "Nodes":
|
||||
return <SwarmNodes id={id} titleOther={Selector} _search={_search} />;
|
||||
case "Services":
|
||||
return (
|
||||
<SwarmServices id={id} titleOther={Selector} _search={_search} />
|
||||
);
|
||||
case "Tasks":
|
||||
return <SwarmTasks id={id} titleOther={Selector} _search={_search} />;
|
||||
case "Secrets":
|
||||
return <SwarmSecrets id={id} titleOther={Selector} _search={_search} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
<Component />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
79
frontend/src/components/resources/swarm/info/nodes.tsx
Normal file
79
frontend/src/components/resources/swarm/info/nodes.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
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";
|
||||
|
||||
export const SwarmNodes = ({
|
||||
id,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const nodes =
|
||||
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
|
||||
const filtered = filterBySplit(
|
||||
nodes,
|
||||
search,
|
||||
(node) => node.Spec?.Name ?? node.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="server-nodes"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => row.original.Spec?.Name ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Status",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
row.original.Status ? row.original.Status.State : "Unknown",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
71
frontend/src/components/resources/swarm/info/secrets.tsx
Normal file
71
frontend/src/components/resources/swarm/info/secrets.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
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";
|
||||
|
||||
export const SwarmSecrets = ({
|
||||
id,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const secrets =
|
||||
useRead("ListSwarmSecrets", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
|
||||
const filtered = filterBySplit(
|
||||
secrets,
|
||||
search,
|
||||
(secret) => secret.Spec?.Name ?? secret.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="server-secrets"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="ID" />
|
||||
),
|
||||
cell: ({ row }) => row.original.Spec?.Name ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
79
frontend/src/components/resources/swarm/info/services.tsx
Normal file
79
frontend/src/components/resources/swarm/info/services.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
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";
|
||||
|
||||
export const SwarmServices = ({
|
||||
id,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
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.Spec?.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="server-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="ID" />
|
||||
),
|
||||
cell: ({ row }) => row.original.Spec?.Name ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ServiceStatus",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => row.original.UpdateStatus?.State ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
59
frontend/src/components/resources/swarm/info/tasks.tsx
Normal file
59
frontend/src/components/resources/swarm/info/tasks.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
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";
|
||||
|
||||
export const SwarmTasks = ({
|
||||
id,
|
||||
titleOther,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
titleOther: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const [search, setSearch] = _search;
|
||||
const tasks =
|
||||
useRead("ListSwarmTasks", { swarm: id }, { refetchInterval: 10_000 })
|
||||
.data ?? [];
|
||||
|
||||
const filtered = filterBySplit(tasks, search, (task) => task.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="server-tasks"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "ID",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
47
frontend/src/components/resources/swarm/tabs.tsx
Normal file
47
frontend/src/components/resources/swarm/tabs.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useLocalStorage } from "@lib/hooks";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
MobileFriendlyTabsSelector,
|
||||
TabNoContent,
|
||||
} from "@ui/mobile-friendly-tabs";
|
||||
import { SwarmConfig } from "./config";
|
||||
import { SwarmInfo } from "./info";
|
||||
|
||||
type SwarmTabsView = "Config" | "Info";
|
||||
|
||||
export const SwarmTabs = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useLocalStorage<SwarmTabsView>(
|
||||
`swarm-${id}-tab-v1`,
|
||||
"Config"
|
||||
);
|
||||
|
||||
// const swarm_info = useSwarm(id)?.info;
|
||||
|
||||
const tabs = useMemo<TabNoContent<SwarmTabsView>[]>(
|
||||
() => [
|
||||
{
|
||||
value: "Config",
|
||||
},
|
||||
{
|
||||
value: "Info",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const Selector = (
|
||||
<MobileFriendlyTabsSelector
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabsTriggerClassname="w-[110px]"
|
||||
/>
|
||||
);
|
||||
|
||||
switch (view) {
|
||||
case "Config":
|
||||
return <SwarmConfig id={id} titleOther={Selector} />;
|
||||
case "Info":
|
||||
return <SwarmInfo id={id} titleOther={Selector} />;
|
||||
}
|
||||
};
|
||||
@@ -147,6 +147,7 @@ const RecentsDashboard = () => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<ResourceRow type="Swarm" />
|
||||
<ResourceRow type="Server" />
|
||||
<ResourceRow type="Stack" />
|
||||
<ResourceRow type="Deployment" />
|
||||
|
||||
Reference in New Issue
Block a user