mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
feat: msft teams support for critical alerts (#5113)
* feat: msft teams support for critical alerts * ee changes * ee * sqlx prep * multiple teams channels * commit file, not symlink * improve reactivity * docs link * Update ee-repo-ref.txt
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, account, is_oauth, expires_at)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3, expires_at = $7",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08dd2ea6b17a52bce352d6443d7d009cfc9da0d3b2bd1f40d422b550779e5324"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, is_secret, expires_at FROM variable WHERE path = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_secret",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "44a12919fb154055f1142cc078ef131f8a0c9cdb37cfba6283a6718480b02a4b"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value FROM global_settings WHERE name = 'teams'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "84d048ea323758842dc564c700b524d1a5b196a7b77afcb02f46b84b22088bbf"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO global_settings (name, value)\n VALUES ('teams', $1)\n ON CONFLICT (name) DO UPDATE SET value = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9c72b2962d0919353cfe5af710e857d432dff44e343b8f0610208d42ff5afd14"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
ddcfdfc18a9833a5fc4e62ad62a265ef1e06a0aa
|
||||
0c89b8974ff6e1c9eda2134f09d4a03f18b57c15
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DELETE FROM global_settings WHERE name = 'teams';
|
||||
@@ -0,0 +1 @@
|
||||
INSERT INTO global_settings (name, value) VALUES ('teams', '{}');
|
||||
+4
-1
@@ -40,7 +40,7 @@ use windmill_common::{
|
||||
NUGET_CONFIG_SETTING, OAUTH_SETTING, OTEL_SETTING, PIP_INDEX_URL_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING, TEAMS_SETTING
|
||||
},
|
||||
scripts::ScriptLang,
|
||||
stats_ee::schedule_stats,
|
||||
@@ -734,6 +734,9 @@ Windmill Community Edition {GIT_VERSION}
|
||||
SMTP_SETTING => {
|
||||
reload_smtp_config(&db).await;
|
||||
},
|
||||
TEAMS_SETTING => {
|
||||
tracing::info!("Teams setting changed.");
|
||||
},
|
||||
INDEXER_SETTING => {
|
||||
reload_indexer_config(&db).await;
|
||||
},
|
||||
|
||||
@@ -1479,7 +1479,7 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
|
||||
#[cfg(feature = "oauth2")]
|
||||
{
|
||||
let mut l = windmill_api::OAUTH_CLIENTS.write().await;
|
||||
*l = windmill_api::oauth2_ee::build_oauth_clients(&base_url, oauths)
|
||||
*l = windmill_api::oauth2_ee::build_oauth_clients(&base_url, oauths, db).await
|
||||
.map_err(|e| tracing::error!("Error building oauth clients (is the oauth.json mounted and in correct format? Use '{}' as minimal oauth.json): {}", "{}", e))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -3234,6 +3234,22 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
/teams/sync:
|
||||
post:
|
||||
operationId: syncTeams
|
||||
summary: synchronize Microsoft Teams information (teams/channels)
|
||||
tags:
|
||||
- teams
|
||||
responses:
|
||||
'200':
|
||||
description: Teams information successfully synchronized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/TeamInfo'
|
||||
|
||||
/w/{workspace}/resources/create:
|
||||
post:
|
||||
@@ -14012,3 +14028,49 @@ components:
|
||||
format: date-time
|
||||
required:
|
||||
- trigger_kind
|
||||
|
||||
TeamInfo:
|
||||
type: object
|
||||
required:
|
||||
- team_id
|
||||
- team_name
|
||||
- channels
|
||||
properties:
|
||||
team_id:
|
||||
type: string
|
||||
description: The unique identifier of the Microsoft Teams team
|
||||
example: "19:abc123def456@thread.tacv2"
|
||||
team_name:
|
||||
type: string
|
||||
description: The display name of the Microsoft Teams team
|
||||
example: "Engineering Team"
|
||||
channels:
|
||||
type: array
|
||||
description: List of channels within the team
|
||||
items:
|
||||
$ref: '#/components/schemas/ChannelInfo'
|
||||
|
||||
ChannelInfo:
|
||||
type: object
|
||||
required:
|
||||
- channel_id
|
||||
- channel_name
|
||||
- tenant_id
|
||||
- service_url
|
||||
properties:
|
||||
channel_id:
|
||||
type: string
|
||||
description: The unique identifier of the channel
|
||||
example: "19:channel123@thread.tacv2"
|
||||
channel_name:
|
||||
type: string
|
||||
description: The display name of the channel
|
||||
example: "General"
|
||||
tenant_id:
|
||||
type: string
|
||||
description: The Microsoft Teams tenant identifier
|
||||
example: "12345678-1234-1234-1234-123456789012"
|
||||
service_url:
|
||||
type: string
|
||||
description: The service URL for the channel
|
||||
example: "https://smba.trafficmanager.net/amer/12345678-1234-1234-1234-123456789012/"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
variables::decrypt,
|
||||
};
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
use windmill_common::variables::decrypt;
|
||||
use anthropic::AnthropicCache;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
|
||||
@@ -13,10 +13,10 @@ use crate::{
|
||||
resources::get_resource_value_interpolated_internal,
|
||||
users::{require_owner_of_path, OptAuthed},
|
||||
utils::WithStarredInfoQuery,
|
||||
variables::encrypt,
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
use windmill_common::variables::encrypt;
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::{
|
||||
job_helpers_ee::{
|
||||
|
||||
@@ -96,6 +96,8 @@ mod scripts;
|
||||
mod service_logs;
|
||||
mod settings;
|
||||
mod slack_approvals;
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod teams_ee;
|
||||
#[cfg(feature = "smtp")]
|
||||
mod smtp_server_ee;
|
||||
mod static_assets;
|
||||
@@ -435,6 +437,17 @@ pub async fn run_server(
|
||||
jobs::workspace_unauthed_service().layer(cors.clone()),
|
||||
)
|
||||
.route("/slack", post(slack_approvals::slack_app_callback_handler))
|
||||
.nest("/teams", {
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
teams_ee::teams_service()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
Router::new()
|
||||
}
|
||||
})
|
||||
.route(
|
||||
"/w/:workspace_id/jobs/slack_approval/:job_id",
|
||||
get(slack_approvals::request_slack_approval),
|
||||
|
||||
@@ -80,9 +80,10 @@ pub struct AllClients {
|
||||
}
|
||||
|
||||
#[cfg(feature = "oauth2")]
|
||||
pub fn build_oauth_clients(
|
||||
pub async fn build_oauth_clients(
|
||||
_base_url: &str,
|
||||
_oauths_from_config: Option<HashMap<String, OAuthClient>>,
|
||||
_db: &DB,
|
||||
) -> anyhow::Result<AllClients> {
|
||||
// Implementation is not open source
|
||||
return Ok(AllClients {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
use axum::Router;
|
||||
|
||||
pub fn teams_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
@@ -31,7 +31,7 @@ use windmill_common::{
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use magic_crypt::{MagicCrypt256, MagicCryptError, MagicCryptTrait};
|
||||
use windmill_common::variables::{decrypt, encrypt};
|
||||
use serde::Deserialize;
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
@@ -676,17 +676,3 @@ pub async fn get_value_internal<'c>(
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {
|
||||
mc.encrypt_str_to_base64(value)
|
||||
}
|
||||
|
||||
pub fn decrypt(mc: &MagicCrypt256, value: String) -> Result<String> {
|
||||
mc.decrypt_base64_to_string(value).map_err(|e| match e {
|
||||
MagicCryptError::DecryptError(_) => Error::InternalErr(
|
||||
"Could not decrypt value. The value may have been encrypted with a different key."
|
||||
.to_string(),
|
||||
),
|
||||
_ => Error::InternalErr(e.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ use windmill_git_sync::handle_deployment_metadata;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::utils::require_admin_or_devops;
|
||||
|
||||
use crate::variables::{decrypt, encrypt};
|
||||
use windmill_common::variables::{decrypt, encrypt};
|
||||
use hyper::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
|
||||
@@ -36,7 +36,7 @@ use windmill_common::{
|
||||
variables::ExportableListableVariable,
|
||||
};
|
||||
|
||||
use crate::variables::decrypt;
|
||||
use windmill_common::variables::decrypt;
|
||||
use hyper::header;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub const PIP_INDEX_URL_SETTING: &str = "pip_index_url";
|
||||
pub const SCIM_TOKEN_SETTING: &str = "scim_token";
|
||||
pub const SAML_METADATA_SETTING: &str = "saml_metadata";
|
||||
pub const SMTP_SETTING: &str = "smtp_settings";
|
||||
pub const TEAMS_SETTING: &str = "teams";
|
||||
pub const INDEXER_SETTING: &str = "indexer_settings";
|
||||
pub const TIMEOUT_WAIT_RESULT_SETTING: &str = "timeout_wait_result";
|
||||
|
||||
@@ -94,3 +95,21 @@ pub const ENV_SETTINGS: [&str; 54] = [
|
||||
"OTEL_TRACING",
|
||||
"OTEL_LOGS",
|
||||
];
|
||||
|
||||
use crate::error;
|
||||
use sqlx::Pool;
|
||||
use sqlx::postgres::Postgres;
|
||||
|
||||
pub async fn load_value_from_global_settings(
|
||||
db: &Pool<Postgres>,
|
||||
setting_name: &str,
|
||||
) -> error::Result<Option<serde_json::Value>> {
|
||||
let r = sqlx::query!(
|
||||
"SELECT value FROM global_settings WHERE name = $1",
|
||||
setting_name
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.map(|x| x.value);
|
||||
Ok(r)
|
||||
}
|
||||
@@ -36,6 +36,7 @@ pub mod job_s3_helpers_ee;
|
||||
pub mod jobs;
|
||||
pub mod more_serde;
|
||||
pub mod oauth2;
|
||||
pub mod teams_ee;
|
||||
pub mod otel_ee;
|
||||
pub mod queue;
|
||||
pub mod s3_helpers;
|
||||
|
||||
@@ -18,6 +18,9 @@ pub const WORKSPACE_SLACK_BOT_TOKEN_PATH: &str = "f/slack_bot/bot_token";
|
||||
|
||||
pub const GLOBAL_SLACK_BOT_TOKEN_PATH: &str = "f/slack_bot/global_bot_token";
|
||||
|
||||
pub const GLOBAL_TEAMS_BOT_TOKEN_PATH: &str = "f/teams_bot/global_bot_token";
|
||||
|
||||
pub const GLOBAL_TEAMS_API_TOKEN_PATH: &str = "f/teams_bot/global_api_token";
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
pub static ref REQUIRE_PREEXISTING_USER_FOR_OAUTH: AtomicBool = AtomicBool::new(std::env::var("REQUIRE_PREEXISTING_USER_FOR_OAUTH")
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use magic_crypt::{MagicCrypt256, MagicCryptError, MagicCryptTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::error;
|
||||
|
||||
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
|
||||
|
||||
@@ -157,6 +158,20 @@ pub async fn decrypt_value_with_mc(
|
||||
})?)
|
||||
}
|
||||
|
||||
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {
|
||||
mc.encrypt_str_to_base64(value)
|
||||
}
|
||||
|
||||
pub fn decrypt(mc: &MagicCrypt256, value: String) -> error::Result<String> {
|
||||
mc.decrypt_base64_to_string(value).map_err(|e| match e {
|
||||
MagicCryptError::DecryptError(_) => error::Error::InternalErr(
|
||||
"Could not decrypt value. The value may have been encrypted with a different key."
|
||||
.to_string(),
|
||||
),
|
||||
_ => error::Error::InternalErr(e.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub const WM_SCHEDULED_FOR: &str = "WM_SCHEDULED_FOR";
|
||||
|
||||
pub async fn get_reserved_variables(
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
'visma',
|
||||
'spotify',
|
||||
'snowflake_oauth',
|
||||
'teams',
|
||||
'xero'
|
||||
]
|
||||
|
||||
@@ -169,10 +170,12 @@
|
||||
<div class="py-1" />
|
||||
<OAuthSetting login={false} name="slack" bind:value={oauths['slack']} />
|
||||
<div class="py-1" />
|
||||
<OAuthSetting login={false} name="teams" bind:value={oauths['teams']} />
|
||||
<div class="py-1" />
|
||||
|
||||
{#each Object.keys(oauths) as k}
|
||||
{#if oauths[k] && !('login_config' in oauths[k])}
|
||||
{#if !['slack'].includes(k) && oauths[k]}
|
||||
{#if !['slack', 'teams'].includes(k) && oauths[k]}
|
||||
<div class="flex flex-col gap-2 pb-4">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
export let center = false
|
||||
export let isSelected = false
|
||||
export let formatExtension = undefined
|
||||
$: iconComponent = APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
|
||||
|
||||
$: iconComponent = name === "teams"
|
||||
? APP_TO_ICON_COMPONENT.ms_teams_webhook
|
||||
: APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
|
||||
</script>
|
||||
|
||||
<div class="truncate flex flex-row gap-2 {center ? 'justify-center items-center' : ''} -pl-2">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
BadgeX,
|
||||
Info,
|
||||
Plus,
|
||||
RefreshCcw,
|
||||
Slack,
|
||||
X
|
||||
} from 'lucide-svelte'
|
||||
@@ -16,7 +17,7 @@
|
||||
import ObjectStoreConfigSettings from './ObjectStoreConfigSettings.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import ConfirmButton from './ConfirmButton.svelte'
|
||||
import { IndexSearchService, SettingService } from '$lib/gen'
|
||||
import { IndexSearchService, SettingService, TeamsService } from '$lib/gen'
|
||||
import { Button, SecondsInput, Skeleton } from './common'
|
||||
import Password from './Password.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
@@ -40,6 +41,8 @@
|
||||
attempted_at: string
|
||||
} | null
|
||||
|
||||
let isFetching = false
|
||||
|
||||
function showSetting(setting: string, values: Record<string, any>) {
|
||||
if (setting == 'dev_instance') {
|
||||
if (values['license_key'] == undefined) {
|
||||
@@ -111,6 +114,49 @@
|
||||
valid: false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTeams() {
|
||||
if (isFetching) return
|
||||
isFetching = true
|
||||
try {
|
||||
$values['teams'] = await TeamsService.syncTeams()
|
||||
} catch (error) {
|
||||
console.error('Error fetching teams:', error)
|
||||
} finally {
|
||||
isFetching = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleTeamChange(event: Event, i: number) {
|
||||
const teamId = (event.target as HTMLSelectElement).value
|
||||
const team = $values['teams'].find((team) => team.team_id === teamId) || null
|
||||
$values['critical_error_channels'][i] = {
|
||||
teams_channel: {
|
||||
team_id: team?.team_id,
|
||||
team_name: team?.team_name,
|
||||
channel_id: team?.channels[0]?.channel_id,
|
||||
channel_name: team?.channels[0]?.channel_name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleChannelChange(event: Event, setting: Setting, i: number) {
|
||||
const channelId = (event.target as HTMLSelectElement).value
|
||||
const team = $values['teams'].find(
|
||||
(team) => team.team_id === $values['critical_error_channels'][i]?.teams_channel?.team_id
|
||||
)
|
||||
const channel = team?.channels.find((channel) => channel.channel_id === channelId) || null
|
||||
if (channelId) {
|
||||
$values['critical_error_channels'][i] = {
|
||||
teams_channel: {
|
||||
team_id: team?.team_id,
|
||||
team_name: team?.team_name,
|
||||
channel_id: channel?.channel_id,
|
||||
channel_name: channel?.channel_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, $values) && !(setting.hiddenIfNull && $values[setting.key] == null)}
|
||||
@@ -339,7 +385,7 @@
|
||||
{#each $values[setting.key] ?? [] as v, i}
|
||||
<div class="flex w-full max-w-lg mt-1 gap-2 items-center">
|
||||
<select
|
||||
class="w-20"
|
||||
class="max-w-24"
|
||||
on:change={(e) => {
|
||||
if (e.target?.['value']) {
|
||||
$values[setting.key][i] = {
|
||||
@@ -347,10 +393,14 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={v && 'slack_channel' in v ? 'slack_channel' : 'email'}
|
||||
value={(() => {
|
||||
if (!v) return 'email'
|
||||
return ['slack_channel', 'teams_channel'].find((type) => type in v) || 'email'
|
||||
})()}
|
||||
>
|
||||
<option value="email">Email</option>
|
||||
<option value="slack_channel">Slack</option>
|
||||
<option value="teams_channel">Teams</option>
|
||||
</select>
|
||||
{#if v && 'slack_channel' in v}
|
||||
<input
|
||||
@@ -365,6 +415,53 @@
|
||||
}}
|
||||
value={v?.slack_channel ?? ''}
|
||||
/>
|
||||
{:else if v && 'teams_channel' in v}
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<select on:change={(e) => handleTeamChange(e, i)}>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
selected={!$values['critical_error_channels'][i]?.teams_channel?.team_id}
|
||||
>Select team</option
|
||||
>
|
||||
{#each $values['teams'] as team}
|
||||
<option
|
||||
value={team.team_id}
|
||||
selected={$values['critical_error_channels'][i]?.teams_channel
|
||||
?.team_id === team.team_id}
|
||||
>
|
||||
{team.team_name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $values['critical_error_channels'][i]?.teams_channel?.team_id}
|
||||
<select
|
||||
id="channel-select"
|
||||
on:change={(e) => handleChannelChange(e, setting, i)}
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
selected={!$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_id}>Select channel</option
|
||||
>
|
||||
{#each $values['teams'].find((team) => team.team_id === $values['critical_error_channels'][i]?.teams_channel?.team_id)?.channels ?? [] as channel}
|
||||
<option
|
||||
value={channel.channel_id}
|
||||
selected={$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_id === channel.channel_id}
|
||||
>
|
||||
{channel.channel_name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<div>
|
||||
<button on:click={fetchTeams} class="flex items-center gap-1 mt-2">
|
||||
<RefreshCcw size={16} class={isFetching ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
type="email"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { scimSamlSetting, settings, settingsKeys, type SettingStorage } from './instanceSettings'
|
||||
import { Button, Tab, TabContent, Tabs } from '$lib/components/common'
|
||||
import { SettingService, SettingsService } from '$lib/gen'
|
||||
import type { TeamInfo } from '$lib/gen/types.gen'
|
||||
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
@@ -83,6 +84,34 @@
|
||||
if (nvalues['indexer_settings'] == undefined) {
|
||||
nvalues['indexer_settings'] = {}
|
||||
}
|
||||
|
||||
if (nvalues['critical_error_channels'] == undefined) {
|
||||
nvalues['critical_error_channels'] = []
|
||||
} else {
|
||||
let teams = ((await SettingService.getGlobal({ key: 'teams' })) as TeamInfo[]) ?? []
|
||||
|
||||
nvalues['teams'] = teams
|
||||
|
||||
nvalues['critical_error_channels'] = nvalues['critical_error_channels'].map((el) => {
|
||||
if (el.teams_channel) {
|
||||
const team = teams.find((team) => team.team_name === el.teams_channel.team_name) || null
|
||||
return {
|
||||
teams_channel: {
|
||||
team_id: team?.team_id,
|
||||
team_name: team?.team_name,
|
||||
channel_id: team?.channels.find(
|
||||
(channel) => channel.channel_id === el.teams_channel.channel_id
|
||||
)?.channel_id,
|
||||
channel_name: team?.channels.find(
|
||||
(channel) => channel.channel_id === el.teams_channel.channel_id
|
||||
)?.channel_name
|
||||
}
|
||||
}
|
||||
}
|
||||
return el
|
||||
})
|
||||
}
|
||||
|
||||
$values = nvalues
|
||||
loading = false
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
$: enabled = value != undefined
|
||||
|
||||
let tenant: string = ''
|
||||
$: name == 'microsoft' && changeTenantId(tenant)
|
||||
$: (name == 'microsoft' || name == 'teams') && changeTenantId(tenant)
|
||||
|
||||
onMount(() => {
|
||||
try {
|
||||
@@ -24,6 +24,9 @@
|
||||
) {
|
||||
tenant = value['login_config']['auth_url'].split('/')[3]
|
||||
}
|
||||
if (name === 'teams' && value?.tenant) {
|
||||
tenant = value.tenant
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Could not set tenantId', e)
|
||||
}
|
||||
@@ -32,13 +35,20 @@
|
||||
function changeTenantId(tenant: string) {
|
||||
if (value && tenant) {
|
||||
if (tenant != '') {
|
||||
value = {
|
||||
...value,
|
||||
login_config: {
|
||||
auth_url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
||||
token_url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
userinfo_url: `https://graph.microsoft.com/oidc/userinfo`,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
if (name === "teams") {
|
||||
value = {
|
||||
...value,
|
||||
tenant
|
||||
}
|
||||
} else {
|
||||
value = {
|
||||
...value,
|
||||
login_config: {
|
||||
auth_url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
|
||||
token_url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
userinfo_url: `https://graph.microsoft.com/oidc/userinfo`,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -67,7 +77,7 @@
|
||||
>
|
||||
{#if enabled}
|
||||
<div class="p-2 rounded border mb-4">
|
||||
{#if name != 'slack'}
|
||||
{#if name != 'slack' && name != 'teams'}
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Custom Name</span>
|
||||
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
|
||||
@@ -81,7 +91,7 @@
|
||||
<span class="text-primary font-semibold text-sm">Client Secret</span>
|
||||
<input type="text" placeholder="Client Secret" bind:value={value['secret']} />
|
||||
</label>
|
||||
{#if name == 'microsoft'}
|
||||
{#if name == 'microsoft' || name == 'teams'}
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Tenant Id</span>
|
||||
<input type="text" placeholder="Tenant Id" bind:value={tenant} />
|
||||
@@ -166,6 +176,13 @@
|
||||
under "Delegated Permissions".
|
||||
</div>
|
||||
</CollapseLink>
|
||||
{:else if name == 'teams'}
|
||||
<CollapseLink text="Instructions">
|
||||
<div class="text-sm text-secondary border p-2">
|
||||
Follow this guide on <a href="https://www.windmill.dev/docs/misc/setup_oauth#microsoft-teams" target="_blank"
|
||||
>Windmill Docs</a> to create a new Microsoft Teams App. Then paste Client ID, Tenant ID, and Client Secret here.
|
||||
</div>
|
||||
</CollapseLink>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -271,7 +271,7 @@ export const settings: Record<string, Setting[]> = {
|
||||
{
|
||||
label: 'Critical alert channels',
|
||||
description:
|
||||
'Channels to send critical alerts to. SMTP and Slack must be configured below. <a href="https://www.windmill.dev/docs/core_concepts/critical_alerts">Learn more</a>',
|
||||
'Channels to send critical alerts to. SMTP, Slack or Microsoft Teams must be configured below. <a href="https://www.windmill.dev/docs/core_concepts/critical_alerts">Learn more</a>',
|
||||
key: 'critical_error_channels',
|
||||
fieldType: 'critical_error_channels',
|
||||
storage: 'setting',
|
||||
|
||||
Reference in New Issue
Block a user