Compare commits

..

2 Commits

Author SHA1 Message Date
Maxwell Becker
64d13666a9 1.16.10 (#178)
* send alert on auto update

* scrolling / capturing monaco editors

* deployed services has correct image

* serde default services for backward compat

* improve auto update config
2024-11-07 23:59:52 -08:00
mbecker20
2b2f354a3c add ImageUpdateAvailable filter to alert page 2024-11-05 01:30:55 -05:00
20 changed files with 450 additions and 116 deletions

26
Cargo.lock generated
View File

@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"axum",
@@ -847,7 +847,7 @@ dependencies = [
[[package]]
name = "cache"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"tokio",
@@ -951,7 +951,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"komodo_client",
"run_command",
@@ -1363,7 +1363,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"thiserror",
]
@@ -1447,7 +1447,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"serror",
]
@@ -1579,7 +1579,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"cache",
@@ -2200,7 +2200,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"clap",
@@ -2216,7 +2216,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2247,7 +2247,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2307,7 +2307,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2395,7 +2395,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"komodo_client",
@@ -3101,7 +3101,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"komodo_client",
@@ -4876,7 +4876,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.16.9"
version = "1.16.10"
dependencies = [
"anyhow",
"komodo_client",

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "1.16.9"
version = "1.16.10"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"

View File

@@ -100,6 +100,16 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!("⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}")
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!("⬆ Deployment **{name}** was updated automatically ⏫\nserver: **{server_name}**\nimage: **{image}**\n{link}")
}
AlertData::StackStateChange {
id,
name,
@@ -123,6 +133,19 @@ pub async fn send_alert(
let link = resource_link(ResourceTargetVariant::Stack, id);
format!("⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}")
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
format!("⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}")
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,

View File

@@ -216,6 +216,27 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_name,
server_id: _server_id,
image,
} => {
let text =
format!("⬆ Deployment *{name}* was updated automatically ⏫");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\nimage: *{image}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Deployment,
id,
)),
];
(text, blocks.into())
}
AlertData::StackStateChange {
name,
server_name,
@@ -259,6 +280,30 @@ pub async fn send_alert(
];
(text, blocks.into())
}
AlertData::StackAutoUpdated {
id,
name,
server_name,
server_id: _server_id,
images,
} => {
let text =
format!("⬆ Stack *{name}* was updated automatically ⏫");
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images = images.join(", ");
let blocks = vec![
Block::header(text.clone()),
Block::section(format!(
"server: *{server_name}*\n{images_label}: *{images}*",
)),
Block::section(resource_link(
ResourceTargetVariant::Stack,
id,
)),
];
(text, blocks.into())
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,

View File

@@ -30,10 +30,7 @@ use crate::{
},
monitor::update_cache_for_server,
resource,
stack::{
execute::execute_compose, get_stack_and_server,
services::extract_services_into_res,
},
stack::{execute::execute_compose, get_stack_and_server},
state::{action_states, db_client, State},
};
@@ -163,6 +160,7 @@ impl Resolve<DeployStack, (User, Update)> for State {
let ComposeUpResponse {
logs,
deployed,
services,
file_contents,
missing_files,
remote_errors,
@@ -181,24 +179,11 @@ impl Resolve<DeployStack, (User, Update)> for State {
update.logs.extend(logs);
let update_info = async {
let latest_services = if !file_contents.is_empty() {
let mut services = Vec::new();
for contents in &file_contents {
if let Err(e) = extract_services_into_res(
&stack.project_name(true),
&contents.contents,
&mut services,
) {
update.push_error_log(
"extract services",
format_serror(&e.context(format!("Failed to extract stack services for compose file path {}. Things probably won't work correctly", contents.path)).into())
);
}
}
services
} else {
let latest_services = if services.is_empty() {
// maybe better to do something else here for services.
stack.info.latest_services.clone()
} else {
services
};
// This ensures to get the latest project name,

View File

@@ -103,11 +103,12 @@ pub async fn update_deployment_cache(
.busy()
.unwrap_or(true)
{
let deployment = deployment.name.clone();
let id = deployment.id.clone();
let server_name = server_name.clone();
tokio::spawn(async move {
if let Err(e) = execute::inner_handler(
match execute::inner_handler(
ExecuteRequest::Deploy(Deploy {
deployment: deployment.clone(),
deployment: deployment.name.clone(),
stop_time: None,
stop_signal: None,
}),
@@ -115,9 +116,37 @@ pub async fn update_deployment_cache(
)
.await
{
warn!(
"Failed to auto update Deployment {deployment} | {e:#}"
)
Ok(_) => {
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Deployment(id.clone()),
data: AlertData::DeploymentAutoUpdated {
id,
name: deployment.name,
server_name,
server_id: deployment.config.server_id,
image,
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record DeploymentAutoUpdated to db | {e:#}"
);
}
send_alerts(&[alert]).await;
}
Err(e) => {
warn!(
"Failed to auto update Deployment {} | {e:#}",
deployment.name
)
}
}
});
}
@@ -152,7 +181,7 @@ pub async fn update_deployment_cache(
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record Deployment update avaialable to db | {e:#}"
"Failed to record DeploymentImageUpdateAvailable to db | {e:#}"
);
}
send_alerts(&[alert]).await;
@@ -257,7 +286,7 @@ pub async fn update_stack_cache(
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record Stack update avaialable to db | {e:#}"
"Failed to record StackImageUpdateAvailable to db | {e:#}"
);
}
send_alerts(&[alert]).await;
@@ -271,21 +300,31 @@ pub async fn update_stack_cache(
}
StackService {
service: service_name.clone(),
image: image.to_string(),
image: image.clone(),
container,
update_available,
}
}).collect::<Vec<_>>();
let update_available =
services_with_containers.iter().any(|service| {
service.update_available
// Only consider running services with available updates
&& service
.container
.as_ref()
.map(|c| c.state == ContainerStateStatusEnum::Running)
.unwrap_or_default()
});
let mut update_available = false;
let mut images_with_update = Vec::new();
for service in services_with_containers.iter() {
if service.update_available {
images_with_update.push(service.image.clone());
// Only allow it to actually trigger an auto update deploy
// if the service is running.
if service
.container
.as_ref()
.map(|c| c.state == ContainerStateStatusEnum::Running)
.unwrap_or_default()
{
update_available = true
}
}
}
let state = get_stack_state_from_containers(
&stack.config.ignore_services,
&services,
@@ -301,11 +340,12 @@ pub async fn update_stack_cache(
.busy()
.unwrap_or(true)
{
let stack = stack.name.clone();
let id = stack.id.clone();
let server_name = server_name.clone();
tokio::spawn(async move {
if let Err(e) = execute::inner_handler(
match execute::inner_handler(
ExecuteRequest::DeployStack(DeployStack {
stack: stack.clone(),
stack: stack.name.clone(),
service: None,
stop_time: None,
}),
@@ -313,7 +353,34 @@ pub async fn update_stack_cache(
)
.await
{
warn!("Failed auto update Stack {stack} | {e:#}")
Ok(_) => {
let ts = komodo_timestamp();
let alert = Alert {
id: Default::default(),
ts,
resolved: true,
resolved_ts: ts.into(),
level: SeverityLevel::Ok,
target: ResourceTarget::Stack(id.clone()),
data: AlertData::StackAutoUpdated {
id,
name: stack.name.clone(),
server_name,
server_id: stack.config.server_id,
images: images_with_update,
},
};
let res = db_client().alerts.insert_one(&alert).await;
if let Err(e) = res {
error!(
"Failed to record StackAutoUpdated to db | {e:#}"
);
}
send_alerts(&[alert]).await;
}
Err(e) => {
warn!("Failed auto update Stack {} | {e:#}", stack.name)
}
}
});
}

View File

@@ -5,8 +5,14 @@ use command::run_komodo_command;
use formatting::format_serror;
use git::environment;
use komodo_client::entities::{
all_logs_success, environment_vars_from_str, stack::Stack,
to_komodo_name, update::Log, CloneArgs, FileContents,
all_logs_success, environment_vars_from_str,
stack::{
ComposeFile, ComposeService, ComposeServiceDeploy, Stack,
StackServiceNames,
},
to_komodo_name,
update::Log,
CloneArgs, FileContents,
};
use periphery_client::api::{
compose::ComposeUpResponse,
@@ -150,7 +156,66 @@ pub async fn compose_up(
output
});
// Build images before destroying to minimize downtime.
// Uses 'docker compose config' command to extract services (including image)
// after performing interpolation
{
let command = format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} config --format json",
);
let config_log = run_komodo_command(
"compose build",
run_directory.as_ref(),
command,
false,
)
.await;
if !config_log.success {
res.logs.push(config_log);
return Err(anyhow!(
"Failed to validate compose files, stopping the run."
));
}
let compose =
serde_json::from_str::<ComposeFile>(&config_log.stdout)
.context("Failed to parse compose contents")?;
for (
service_name,
ComposeService {
container_name,
deploy,
image,
},
) in compose.services
{
let image = image.unwrap_or_default();
match deploy {
Some(ComposeServiceDeploy {
replicas: Some(replicas),
}) if replicas > 1 => {
for i in 1..1 + replicas {
res.services.push(StackServiceNames {
container_name: format!(
"{project_name}-{service_name}-{i}"
),
service_name: format!("{service_name}-{i}"),
image: image.clone(),
});
}
}
_ => {
res.services.push(StackServiceNames {
container_name: container_name.unwrap_or_else(|| {
format!("{project_name}-{service_name}")
}),
service_name,
image,
});
}
}
}
}
// Build images before deploying.
// If this fails, do not continue.
if stack.config.run_build {
let build_extra_args =
@@ -198,7 +263,7 @@ pub async fn compose_up(
}
}
//
// Pull images before deploying
if stack.config.auto_pull {
// Pull images before destroying to minimize downtime.
// If this fails, do not continue.

View File

@@ -158,6 +158,20 @@ pub enum AlertData {
image: String,
},
/// A Deployment has an image update available
DeploymentAutoUpdated {
/// The id of the deployment
id: String,
/// The name of the deployment
name: String,
/// The server id of server that the deployment is on
server_id: String,
/// The server name
server_name: String,
/// The updated image
image: String,
},
/// A stack's state has changed unexpectedly.
StackStateChange {
/// The id of the stack
@@ -190,6 +204,20 @@ pub enum AlertData {
image: String,
},
/// A Stack was auto updated
StackAutoUpdated {
/// The id of the stack
id: String,
/// The name of the stack
name: String,
/// The server id of server that the stack is on
server_id: String,
/// The server name
server_name: String,
/// One or more images that were updated
images: Vec<String>,
},
/// An AWS builder failed to terminate.
AwsBuilderTerminationFailed {
/// The id of the aws instance which failed to terminate

View File

@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "1.16.9",
"version": "1.16.10",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",

View File

@@ -786,8 +786,10 @@ export interface DeploymentConfig {
/** Whether to poll for any updates to the image. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer image is found.
* Whether to automatically redeploy when
* newer a image is found. Will implicitly
* enable `poll_for_updates`, you don't need to
* enable both.
*/
auto_update?: boolean;
/** Whether to send ContainerStateChange alerts for this deployment. */
@@ -1000,6 +1002,19 @@ export type AlertData =
server_name: string;
/** The image with update */
image: string;
}}
/** A Deployment has an image update available */
| { type: "DeploymentAutoUpdated", data: {
/** The id of the deployment */
id: string;
/** The name of the deployment */
name: string;
/** The server id of server that the deployment is on */
server_id: string;
/** The server name */
server_name: string;
/** The updated image */
image: string;
}}
/** A stack's state has changed unexpectedly. */
| { type: "StackStateChange", data: {
@@ -1030,6 +1045,19 @@ export type AlertData =
service: string;
/** The image with update */
image: string;
}}
/** A Stack was auto updated */
| { type: "StackAutoUpdated", data: {
/** The id of the stack */
id: string;
/** The name of the stack */
name: string;
/** The server id of server that the stack is on */
server_id: string;
/** The server name */
server_name: string;
/** One or more images that were updated */
images: string[];
}}
/** An AWS builder failed to terminate. */
| { type: "AwsBuilderTerminationFailed", data: {
@@ -1549,8 +1577,10 @@ export interface StackConfig {
/** Whether to poll for any updates to the images. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer images are found.
* Whether to automatically redeploy when
* newer images are found. Will implicitly
* enable `poll_for_updates`, you don't need to
* enable both.
*/
auto_update?: boolean;
/** Whether to run `docker compose down` before `compose up`. */

View File

@@ -1,5 +1,5 @@
use komodo_client::entities::{
stack::{ComposeProject, Stack},
stack::{ComposeProject, Stack, StackServiceNames},
update::Log,
FileContents, SearchCombinator,
};
@@ -167,8 +167,13 @@ pub struct ComposeUpResponse {
pub missing_files: Vec<String>,
/// The logs produced by the deploy
pub logs: Vec<Log>,
/// whether stack was successfully deployed
/// Whether stack was successfully deployed
pub deployed: bool,
/// The stack services.
///
/// Note. The "image" is after interpolation.
#[serde(default)]
pub services: Vec<StackServiceNames>,
/// The deploy compose file contents if they could be acquired, or empty vec.
pub file_contents: Vec<FileContents>,
/// The error in getting remote file contents at the path, or null

View File

@@ -888,8 +888,10 @@ export interface DeploymentConfig {
/** Whether to poll for any updates to the image. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer image is found.
* Whether to automatically redeploy when
* newer a image is found. Will implicitly
* enable `poll_for_updates`, you don't need to
* enable both.
*/
auto_update?: boolean;
/** Whether to send ContainerStateChange alerts for this deployment. */
@@ -1107,6 +1109,22 @@ export type AlertData =
image: string;
};
}
/** A Deployment has an image update available */
| {
type: "DeploymentAutoUpdated";
data: {
/** The id of the deployment */
id: string;
/** The name of the deployment */
name: string;
/** The server id of server that the deployment is on */
server_id: string;
/** The server name */
server_name: string;
/** The updated image */
image: string;
};
}
/** A stack's state has changed unexpectedly. */
| {
type: "StackStateChange";
@@ -1143,6 +1161,22 @@ export type AlertData =
image: string;
};
}
/** A Stack was auto updated */
| {
type: "StackAutoUpdated";
data: {
/** The id of the stack */
id: string;
/** The name of the stack */
name: string;
/** The server id of server that the stack is on */
server_id: string;
/** The server name */
server_name: string;
/** One or more images that were updated */
images: string[];
};
}
/** An AWS builder failed to terminate. */
| {
type: "AwsBuilderTerminationFailed";
@@ -1638,8 +1672,10 @@ export interface StackConfig {
/** Whether to poll for any updates to the images. */
poll_for_updates?: boolean;
/**
* Whether to automatically redeploy when a
* newer images are found.
* Whether to automatically redeploy when
* newer images are found. Will implicitly
* enable `poll_for_updates`, you don't need to
* enable both.
*/
auto_update?: boolean;
/** Whether to run `docker compose down` before `compose up`. */

View File

@@ -1164,30 +1164,20 @@ export const RenameResource = ({
const [name, set] = useState("");
return (
<div className="flex items-center justify-between">
<div className="w-full">
Rename{" "}
{type === "ServerTemplate"
? "Template"
: type === "ResourceSync"
? "Sync"
: type}
</div>
<div className="flex gap-4 w-full justify-end">
<Input
value={name}
onChange={(e) => set(e.target.value)}
className="w-96"
placeholder="Enter new name"
/>
<ConfirmButton
title="Rename"
icon={<Pen className="w-4 h-4" />}
disabled={!name || isPending}
loading={isPending}
onClick={() => mutate({ id, name })}
/>
</div>
<div className="flex gap-4 w-full justify-end flex-wrap">
<Input
value={name}
onChange={(e) => set(e.target.value)}
className="w-96"
placeholder="Enter new name"
/>
<ConfirmButton
title="Rename"
icon={<Pen className="w-4 h-4" />}
disabled={!name || isPending}
loading={isPending}
onClick={() => mutate({ id, name })}
/>
</div>
);
};

View File

@@ -7,6 +7,7 @@ import * as prettier from "prettier/standalone";
import * as pluginTypescript from "prettier/plugins/typescript";
import * as pluginEstree from "prettier/plugins/estree";
import * as pluginYaml from "prettier/plugins/yaml";
import { useWindowDimensions } from "@lib/hooks";
const MIN_EDITOR_HEIGHT = 56;
// const MAX_EDITOR_HEIGHT = 500;
@@ -39,6 +40,7 @@ export const MonacoEditor = ({
minHeight?: number;
className?: string;
}) => {
const dimensions = useWindowDimensions();
const [editor, setEditor] =
useState<monaco.editor.IStandaloneCodeEditor | null>(null);
@@ -100,9 +102,13 @@ export const MonacoEditor = ({
const contentHeight = line_count * 18 + 30;
const containerNode = editor.getContainerDomNode();
containerNode.style.height = `${Math.max(
Math.ceil(contentHeight),
minHeight ?? MIN_EDITOR_HEIGHT
// containerNode.style.height = `${Math.max(
// Math.ceil(contentHeight),
// minHeight ?? MIN_EDITOR_HEIGHT
// )}px`;
containerNode.style.height = `${Math.min(
Math.max(Math.ceil(contentHeight), minHeight ?? MIN_EDITOR_HEIGHT),
Math.floor(dimensions.height * (3 / 5))
)}px`;
}, [editor, line_count]);
@@ -116,7 +122,7 @@ export const MonacoEditor = ({
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
minimap: { enabled: false },
scrollbar: { alwaysConsumeMouseWheel: false },
// scrollbar: { alwaysConsumeMouseWheel: false },
scrollBeyondLastLine: false,
folding: false,
automaticLayout: true,

View File

@@ -10,12 +10,14 @@ const ALERT_TYPES: Types.AlertData["type"][] = [
"ServerCpu",
"ServerMem",
"ServerDisk",
// State change
"ContainerStateChange",
// Stack
"StackStateChange",
// Updates
"DeploymentImageUpdateAvailable",
"StackImageUpdateAvailable",
"StackAutoUpdated",
// Deployment
"ContainerStateChange",
"DeploymentImageUpdateAvailable",
"DeploymentAutoUpdated",
// Misc
"AwsBuilderTerminationFailed",
"ResourceSyncPendingUpdates",

View File

@@ -6,6 +6,7 @@ import {
AddExtraArgMenu,
ConfigItem,
ConfigList,
ConfigSwitch,
InputList,
} from "@components/config/util";
import { ImageConfig } from "./components/image";
@@ -46,6 +47,7 @@ export const DeploymentConfig = ({
const network = update.network ?? config.network;
const hide_ports = network === "host" || network === "none";
const auto_update = update.auto_update ?? config.auto_update ?? false;
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
@@ -229,9 +231,18 @@ export const DeploymentConfig = ({
},
{
label: "Auto Update",
hidden: (update.image ?? config.image)?.type === "Build",
components: {
poll_for_updates: !(update.auto_update ?? config.auto_update) && {
description: "Check for updates to the image on an interval.",
poll_for_updates: (poll, set) => {
return (
<ConfigSwitch
label="Poll for Updates"
description="Check for updates to the image on an interval."
value={auto_update || poll}
onChange={(poll_for_updates) => set({ poll_for_updates })}
disabled={disabled || auto_update}
/>
);
},
auto_update: {
description: "Trigger a redeploy if a newer image is found.",

View File

@@ -3,6 +3,7 @@ import {
AccountSelectorConfig,
ConfigItem,
ConfigList,
ConfigSwitch,
ProviderSelectorConfig,
WebhookBuilder,
} from "@components/config/util";
@@ -92,7 +93,7 @@ export const ResourceSyncConfig = ({
const integration = getWebhookIntegration(integrations, git_provider);
const mode = getSyncMode(update, config);
const managed = update.managed ?? config.managed;
const managed = update.managed ?? config.managed ?? false;
const setMode = (mode: SyncMode) => {
if (mode === "Files On Server") {
@@ -170,10 +171,16 @@ export const ResourceSyncConfig = ({
const general_common: ConfigComponent<Types.ResourceSyncConfig> = {
label: "General",
components: {
delete: !managed && {
label: "Delete Unmatched Resources",
description:
"Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.",
delete: (delete_mode, set) => {
return (
<ConfigSwitch
label="Delete Unmatched Resources"
description="Executions will delete any resources not found in the resource files. Only use this when using one sync for everything."
value={managed || delete_mode}
onChange={(delete_mode) => set({ delete: delete_mode })}
disabled={disabled || managed}
/>
);
},
managed: {
label: "Managed",

View File

@@ -4,6 +4,7 @@ import {
AddExtraArgMenu,
ConfigItem,
ConfigList,
ConfigSwitch,
InputList,
ProviderSelectorConfig,
SystemCommand,
@@ -193,6 +194,8 @@ export const StackConfig = ({
},
};
const auto_update = update.auto_update ?? config.auto_update ?? false;
const general_common: ConfigComponent<Types.StackConfig>[] = [
{
label: "Environment",
@@ -239,8 +242,16 @@ export const StackConfig = ({
{
label: "Auto Update",
components: {
poll_for_updates: !(update.auto_update ?? config.auto_update) && {
description: "Check for updates to the image on an interval.",
poll_for_updates: (poll, set) => {
return (
<ConfigSwitch
label="Poll for Updates"
description="Check for updates to the image on an interval."
value={auto_update || poll}
onChange={(poll_for_updates) => set({ poll_for_updates })}
disabled={disabled || auto_update}
/>
);
},
auto_update: {
description: "Trigger a redeploy if a newer image is found.",

View File

@@ -463,3 +463,22 @@ const WEBHOOK_ID_OR_NAME_ATOM = atomWithStorage<WebhookIdOrName>(
export const useWebhookIdOrName = () => {
return useAtom<WebhookIdOrName>(WEBHOOK_ID_OR_NAME_ATOM);
};
export type Dimensions = { width: number; height: number };
export const useWindowDimensions = () => {
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
useEffect(() => {
const callback = () => {
setDimensions({
width: window.screen.availWidth,
height: window.screen.availHeight,
});
}
callback();
window.addEventListener("resize", callback);
return () => {
window.removeEventListener("resize", callback);
}
}, []);
return dimensions;
}

View File

@@ -27,8 +27,12 @@ import { ResourceSelector } from "@components/resources/common";
const ALERT_TYPES_BY_RESOURCE: { [key: string]: Types.AlertData["type"][] } = {
Server: ["ServerUnreachable", "ServerCpu", "ServerMem", "ServerDisk"],
Stack: ["StackStateChange"],
Deployment: ["ContainerStateChange"],
Stack: ["StackStateChange", "StackImageUpdateAvailable", "StackAutoUpdated"],
Deployment: [
"ContainerStateChange",
"DeploymentImageUpdateAvailable",
"DeploymentAutoUpdated",
],
Build: ["BuildFailed"],
Repo: ["RepoBuildFailed"],
ResourceSync: ["ResourceSyncPendingUpdates"],
@@ -64,7 +68,7 @@ export const AlertsPage = () => {
});
const alert_types: string[] = type
? ALERT_TYPES_BY_RESOURCE[type] ?? FALLBACK_ALERT_TYPES
? (ALERT_TYPES_BY_RESOURCE[type] ?? FALLBACK_ALERT_TYPES)
: FALLBACK_ALERT_TYPES;
return (