diff --git a/.gitignore b/.gitignore index 11a20b6..1987e85 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ postgresus-data/ .env pgdata/ docker-compose.yml -node_modules/ \ No newline at end of file +node_modules/ +.idea \ No newline at end of file diff --git a/backend/internal/features/notifiers/enums.go b/backend/internal/features/notifiers/enums.go index bfff440..5dc38e8 100644 --- a/backend/internal/features/notifiers/enums.go +++ b/backend/internal/features/notifiers/enums.go @@ -8,4 +8,5 @@ const ( NotifierTypeWebhook NotifierType = "WEBHOOK" NotifierTypeSlack NotifierType = "SLACK" NotifierTypeDiscord NotifierType = "DISCORD" + NotifierTypeTeams NotifierType = "TEAMS" ) diff --git a/backend/internal/features/notifiers/model.go b/backend/internal/features/notifiers/model.go index 32baf16..870a524 100644 --- a/backend/internal/features/notifiers/model.go +++ b/backend/internal/features/notifiers/model.go @@ -6,6 +6,7 @@ import ( discord_notifier "postgresus-backend/internal/features/notifiers/models/discord" "postgresus-backend/internal/features/notifiers/models/email_notifier" slack_notifier "postgresus-backend/internal/features/notifiers/models/slack" + teams_notifier "postgresus-backend/internal/features/notifiers/models/teams" telegram_notifier "postgresus-backend/internal/features/notifiers/models/telegram" webhook_notifier "postgresus-backend/internal/features/notifiers/models/webhook" @@ -20,11 +21,12 @@ type Notifier struct { LastSendError *string `json:"lastSendError" gorm:"column:last_send_error;type:text"` // specific notifier - TelegramNotifier *telegram_notifier.TelegramNotifier `json:"telegramNotifier" gorm:"foreignKey:NotifierID"` - EmailNotifier *email_notifier.EmailNotifier `json:"emailNotifier" gorm:"foreignKey:NotifierID"` - WebhookNotifier *webhook_notifier.WebhookNotifier `json:"webhookNotifier" gorm:"foreignKey:NotifierID"` - SlackNotifier *slack_notifier.SlackNotifier `json:"slackNotifier" gorm:"foreignKey:NotifierID"` - DiscordNotifier *discord_notifier.DiscordNotifier `json:"discordNotifier" gorm:"foreignKey:NotifierID"` + TelegramNotifier *telegram_notifier.TelegramNotifier `json:"telegramNotifier" gorm:"foreignKey:NotifierID"` + EmailNotifier *email_notifier.EmailNotifier `json:"emailNotifier" gorm:"foreignKey:NotifierID"` + WebhookNotifier *webhook_notifier.WebhookNotifier `json:"webhookNotifier" gorm:"foreignKey:NotifierID"` + SlackNotifier *slack_notifier.SlackNotifier `json:"slackNotifier" gorm:"foreignKey:NotifierID"` + DiscordNotifier *discord_notifier.DiscordNotifier `json:"discordNotifier" gorm:"foreignKey:NotifierID"` + TeamsNotifier *teams_notifier.TeamsNotifier `json:"teamsNotifier,omitempty" gorm:"foreignKey:NotifierID;constraint:OnDelete:CASCADE"` } func (n *Notifier) TableName() string { @@ -64,6 +66,8 @@ func (n *Notifier) getSpecificNotifier() NotificationSender { return n.SlackNotifier case NotifierTypeDiscord: return n.DiscordNotifier + case NotifierTypeTeams: + return n.TeamsNotifier default: panic("unknown notifier type: " + string(n.NotifierType)) } diff --git a/backend/internal/features/notifiers/models/teams/model.go b/backend/internal/features/notifiers/models/teams/model.go new file mode 100644 index 0000000..88287b2 --- /dev/null +++ b/backend/internal/features/notifiers/models/teams/model.go @@ -0,0 +1,96 @@ +package teams_notifier + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/http" + "net/url" + + "github.com/google/uuid" +) + +type TeamsNotifier struct { + NotifierID uuid.UUID `gorm:"type:uuid;primaryKey;column:notifier_id" json:"notifierId"` + WebhookURL string `gorm:"type:text;not null;column:power_automate_url" json:"powerAutomateUrl"` +} + +func (TeamsNotifier) TableName() string { + return "teams_notifiers" +} + +func (n *TeamsNotifier) Validate() error { + if n.WebhookURL == "" { + return errors.New("webhook_url is required") + } + u, err := url.Parse(n.WebhookURL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") { + return errors.New("invalid webhook_url") + } + return nil +} + +type cardAttachment struct { + ContentType string `json:"contentType"` + Content interface{} `json:"content"` +} + +type payload struct { + Title string `json:"title"` + Text string `json:"text"` + Attachments []cardAttachment `json:"attachments,omitempty"` +} + +func (n *TeamsNotifier) Send(logger *slog.Logger, heading, message string) error { + if err := n.Validate(); err != nil { + return err + } + + card := map[string]any{ + "type": "AdaptiveCard", + "version": "1.4", + "body": []any{ + map[string]any{ + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": heading, + }, + map[string]any{"type": "TextBlock", "wrap": true, "text": message}, + }, + } + + p := payload{ + Title: heading, + Text: message, + Attachments: []cardAttachment{ + {ContentType: "application/vnd.microsoft.card.adaptive", Content: card}, + }, + } + + body, _ := json.Marshal(p) + req, err := http.NewRequest(http.MethodPost, n.WebhookURL, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + logger.Error("failed to close response body", "error", closeErr) + } + }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("teams webhook returned status %d", resp.StatusCode) + } + + return nil +} diff --git a/backend/internal/features/notifiers/repository.go b/backend/internal/features/notifiers/repository.go index 6b4f9de..2d18a71 100644 --- a/backend/internal/features/notifiers/repository.go +++ b/backend/internal/features/notifiers/repository.go @@ -13,6 +13,7 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) { db := storage.GetDb() err := db.Transaction(func(tx *gorm.DB) error { + switch notifier.NotifierType { case NotifierTypeTelegram: if notifier.TelegramNotifier != nil { @@ -34,30 +35,36 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) { if notifier.DiscordNotifier != nil { notifier.DiscordNotifier.NotifierID = notifier.ID } + case NotifierTypeTeams: + if notifier.TeamsNotifier != nil { + notifier.TeamsNotifier.NotifierID = notifier.ID + } } if notifier.ID == uuid.Nil { - if err := tx.Create(notifier). + if err := tx. Omit( "TelegramNotifier", "EmailNotifier", "WebhookNotifier", "SlackNotifier", "DiscordNotifier", + "TeamsNotifier", ). - Error; err != nil { + Create(notifier).Error; err != nil { return err } } else { - if err := tx.Save(notifier). + if err := tx. Omit( "TelegramNotifier", "EmailNotifier", "WebhookNotifier", "SlackNotifier", "DiscordNotifier", + "TeamsNotifier", ). - Error; err != nil { + Save(notifier).Error; err != nil { return err } } @@ -65,39 +72,46 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) { switch notifier.NotifierType { case NotifierTypeTelegram: if notifier.TelegramNotifier != nil { - notifier.TelegramNotifier.NotifierID = notifier.ID // Ensure ID is set + notifier.TelegramNotifier.NotifierID = notifier.ID if err := tx.Save(notifier.TelegramNotifier).Error; err != nil { return err } } case NotifierTypeEmail: if notifier.EmailNotifier != nil { - notifier.EmailNotifier.NotifierID = notifier.ID // Ensure ID is set + notifier.EmailNotifier.NotifierID = notifier.ID if err := tx.Save(notifier.EmailNotifier).Error; err != nil { return err } } case NotifierTypeWebhook: if notifier.WebhookNotifier != nil { - notifier.WebhookNotifier.NotifierID = notifier.ID // Ensure ID is set + notifier.WebhookNotifier.NotifierID = notifier.ID if err := tx.Save(notifier.WebhookNotifier).Error; err != nil { return err } } case NotifierTypeSlack: if notifier.SlackNotifier != nil { - notifier.SlackNotifier.NotifierID = notifier.ID // Ensure ID is set + notifier.SlackNotifier.NotifierID = notifier.ID if err := tx.Save(notifier.SlackNotifier).Error; err != nil { return err } } case NotifierTypeDiscord: if notifier.DiscordNotifier != nil { - notifier.DiscordNotifier.NotifierID = notifier.ID // Ensure ID is set + notifier.DiscordNotifier.NotifierID = notifier.ID if err := tx.Save(notifier.DiscordNotifier).Error; err != nil { return err } } + case NotifierTypeTeams: + if notifier.TeamsNotifier != nil { + notifier.TeamsNotifier.NotifierID = notifier.ID + if err := tx.Save(notifier.TeamsNotifier).Error; err != nil { + return err + } + } } return nil @@ -120,6 +134,7 @@ func (r *NotifierRepository) FindByID(id uuid.UUID) (*Notifier, error) { Preload("WebhookNotifier"). Preload("SlackNotifier"). Preload("DiscordNotifier"). + Preload("TeamsNotifier"). Where("id = ?", id). First(¬ifier).Error; err != nil { return nil, err @@ -138,6 +153,7 @@ func (r *NotifierRepository) FindByUserID(userID uuid.UUID) ([]*Notifier, error) Preload("WebhookNotifier"). Preload("SlackNotifier"). Preload("DiscordNotifier"). + Preload("TeamsNotifier"). Where("user_id = ?", userID). Order("name ASC"). Find(¬ifiers).Error; err != nil { @@ -149,7 +165,7 @@ func (r *NotifierRepository) FindByUserID(userID uuid.UUID) ([]*Notifier, error) func (r *NotifierRepository) Delete(notifier *Notifier) error { return storage.GetDb().Transaction(func(tx *gorm.DB) error { - // Delete specific notifier based on type + switch notifier.NotifierType { case NotifierTypeTelegram: if notifier.TelegramNotifier != nil { @@ -181,9 +197,14 @@ func (r *NotifierRepository) Delete(notifier *Notifier) error { return err } } + case NotifierTypeTeams: + if notifier.TeamsNotifier != nil { + if err := tx.Delete(notifier.TeamsNotifier).Error; err != nil { + return err + } + } } - // Delete the main notifier return tx.Delete(notifier).Error }) } diff --git a/backend/migrations/20250906152330_add_ms_teams_notifier.sql b/backend/migrations/20250906152330_add_ms_teams_notifier.sql new file mode 100644 index 0000000..cc8d2c2 --- /dev/null +++ b/backend/migrations/20250906152330_add_ms_teams_notifier.sql @@ -0,0 +1,20 @@ +-- +goose Up +-- +goose StatementBegin + +CREATE TABLE teams_notifiers ( + notifier_id UUID PRIMARY KEY, + power_automate_url TEXT NOT NULL +); + +ALTER TABLE teams_notifiers + ADD CONSTRAINT fk_teams_notifiers_notifier + FOREIGN KEY (notifier_id) + REFERENCES notifiers (id) + ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS teams_notifiers; +-- +goose StatementEnd diff --git a/frontend/public/icons/notifiers/teams.svg b/frontend/public/icons/notifiers/teams.svg new file mode 100644 index 0000000..4b02f7a --- /dev/null +++ b/frontend/public/icons/notifiers/teams.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/frontend/src/entity/notifiers/index.ts b/frontend/src/entity/notifiers/index.ts index 1ff42ab..ba09989 100644 --- a/frontend/src/entity/notifiers/index.ts +++ b/frontend/src/entity/notifiers/index.ts @@ -17,3 +17,6 @@ export { validateSlackNotifier } from './models/slack/validateSlackNotifier'; export type { DiscordNotifier } from './models/discord/DiscordNotifier'; export { validateDiscordNotifier } from './models/discord/validateDiscordNotifier'; + +export type { TeamsNotifier } from './models/teams/TeamsNotifier'; +export { validateTeamsNotifier } from './models/teams/validateTeamsNotifier'; diff --git a/frontend/src/entity/notifiers/models/Notifier.ts b/frontend/src/entity/notifiers/models/Notifier.ts index 5adea8e..6675e26 100644 --- a/frontend/src/entity/notifiers/models/Notifier.ts +++ b/frontend/src/entity/notifiers/models/Notifier.ts @@ -2,6 +2,7 @@ import type { NotifierType } from './NotifierType'; import type { DiscordNotifier } from './discord/DiscordNotifier'; import type { EmailNotifier } from './email/EmailNotifier'; import type { SlackNotifier } from './slack/SlackNotifier'; +import type { TeamsNotifier } from './teams/TeamsNotifier'; import type { TelegramNotifier } from './telegram/TelegramNotifier'; import type { WebhookNotifier } from './webhook/WebhookNotifier'; @@ -17,4 +18,5 @@ export interface Notifier { webhookNotifier?: WebhookNotifier; slackNotifier?: SlackNotifier; discordNotifier?: DiscordNotifier; + teamsNotifier?: TeamsNotifier; } diff --git a/frontend/src/entity/notifiers/models/NotifierType.ts b/frontend/src/entity/notifiers/models/NotifierType.ts index 31839ab..23368dd 100644 --- a/frontend/src/entity/notifiers/models/NotifierType.ts +++ b/frontend/src/entity/notifiers/models/NotifierType.ts @@ -4,4 +4,5 @@ export enum NotifierType { WEBHOOK = 'WEBHOOK', SLACK = 'SLACK', DISCORD = 'DISCORD', + TEAMS = 'TEAMS', } diff --git a/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts b/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts index 5fa5adb..98e80e4 100644 --- a/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts +++ b/frontend/src/entity/notifiers/models/getNotifierLogoFromType.ts @@ -12,6 +12,8 @@ export const getNotifierLogoFromType = (type: NotifierType) => { return '/icons/notifiers/slack.svg'; case NotifierType.DISCORD: return '/icons/notifiers/discord.svg'; + case NotifierType.TEAMS: + return '/icons/notifiers/teams.svg'; default: return ''; } diff --git a/frontend/src/entity/notifiers/models/getNotifierNameFromType.ts b/frontend/src/entity/notifiers/models/getNotifierNameFromType.ts index f4952e6..7c24cb7 100644 --- a/frontend/src/entity/notifiers/models/getNotifierNameFromType.ts +++ b/frontend/src/entity/notifiers/models/getNotifierNameFromType.ts @@ -12,6 +12,8 @@ export const getNotifierNameFromType = (type: NotifierType) => { return 'Slack'; case NotifierType.DISCORD: return 'Discord'; + case NotifierType.TEAMS: + return 'Teams'; default: return ''; } diff --git a/frontend/src/entity/notifiers/models/teams/TeamsNotifier.ts b/frontend/src/entity/notifiers/models/teams/TeamsNotifier.ts new file mode 100644 index 0000000..9b1bbea --- /dev/null +++ b/frontend/src/entity/notifiers/models/teams/TeamsNotifier.ts @@ -0,0 +1,7 @@ +export interface TeamsNotifier { + /** Power Automate HTTP endpoint: + * trigger = "When an HTTP request is received" + * e.g. https://prod-00.westeurope.logic.azure.com/workflows/... + */ + powerAutomateUrl: string; +} diff --git a/frontend/src/entity/notifiers/models/teams/validateTeamsNotifier.ts b/frontend/src/entity/notifiers/models/teams/validateTeamsNotifier.ts new file mode 100644 index 0000000..28cac0f --- /dev/null +++ b/frontend/src/entity/notifiers/models/teams/validateTeamsNotifier.ts @@ -0,0 +1,16 @@ +import type { TeamsNotifier } from './TeamsNotifier'; + +export const validateTeamsNotifier = (notifier: TeamsNotifier): boolean => { + if (!notifier?.powerAutomateUrl) { + return false; + } + + try { + const u = new URL(notifier.powerAutomateUrl); + if (u.protocol !== 'http:' && u.protocol !== 'https:') return false; + } catch { + return false; + } + + return true; +}; diff --git a/frontend/src/features/databases/ui/DatabaseCardComponent.tsx b/frontend/src/features/databases/ui/DatabaseCardComponent.tsx index b04ca8c..4e5a0e3 100644 --- a/frontend/src/features/databases/ui/DatabaseCardComponent.tsx +++ b/frontend/src/features/databases/ui/DatabaseCardComponent.tsx @@ -1,8 +1,12 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; +import { useEffect, useState } from 'react'; +import { backupConfigApi } from '../../../entity/backups'; import { type Database, DatabaseType } from '../../../entity/databases'; import { HealthStatus } from '../../../entity/databases/model/HealthStatus'; +import type { Storage } from '../../../entity/storages'; +import { getStorageLogoFromType } from '../../../entity/storages/models/getStorageLogoFromType'; import { getUserShortTimeFormat } from '../../../shared/time/getUserTimeFormat'; interface Props { @@ -16,6 +20,8 @@ export const DatabaseCardComponent = ({ selectedDatabaseId, setSelectedDatabaseId, }: Props) => { + const [storage, setStorage] = useState(); + let databaseIcon = ''; let databaseType = ''; @@ -24,6 +30,12 @@ export const DatabaseCardComponent = ({ databaseType = 'PostgreSQL'; } + useEffect(() => { + if (!database.id) return; + + backupConfigApi.getBackupConfigByDbID(database.id).then((res) => setStorage(res?.storage)); + }, [database.id]); + return (
Database type: {databaseType}
- databaseIcon
+ {storage && ( +
+ Storage: + + {storage.name}{' '} + {storage.type && ( + storageIcon + )} + +
+ )} + {database.lastBackupTime && (
Last backup diff --git a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx index 541d9e4..2922dfd 100644 --- a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx @@ -9,6 +9,7 @@ import { validateDiscordNotifier, validateEmailNotifier, validateSlackNotifier, + validateTeamsNotifier, validateTelegramNotifier, validateWebhookNotifier, } from '../../../../entity/notifiers'; @@ -17,6 +18,7 @@ import { ToastHelper } from '../../../../shared/toast'; import { EditDiscordNotifierComponent } from './notifiers/EditDiscordNotifierComponent'; import { EditEmailNotifierComponent } from './notifiers/EditEmailNotifierComponent'; import { EditSlackNotifierComponent } from './notifiers/EditSlackNotifierComponent'; +import { EditTeamsNotifierComponent } from './notifiers/EditTeamsNotifierComponent'; import { EditTelegramNotifierComponent } from './notifiers/EditTelegramNotifierComponent'; import { EditWebhookNotifierComponent } from './notifiers/EditWebhookNotifierComponent'; @@ -90,6 +92,7 @@ export function EditNotifierComponent({ notifier.emailNotifier = undefined; notifier.telegramNotifier = undefined; + notifier.teamsNotifier = undefined; if (type === NotifierType.TELEGRAM) { notifier.telegramNotifier = { @@ -128,6 +131,10 @@ export function EditNotifierComponent({ }; } + if (type === NotifierType.TEAMS) { + notifier.teamsNotifier = { powerAutomateUrl: '' }; + } + setNotifier( JSON.parse( JSON.stringify({ @@ -183,6 +190,10 @@ export function EditNotifierComponent({ return validateDiscordNotifier(notifier.discordNotifier); } + if (notifier.notifierType === NotifierType.TEAMS && notifier.teamsNotifier) { + return validateTeamsNotifier(notifier.teamsNotifier); + } + return false; }; @@ -218,6 +229,7 @@ export function EditNotifierComponent({ { label: 'Webhook', value: NotifierType.WEBHOOK }, { label: 'Slack', value: NotifierType.SLACK }, { label: 'Discord', value: NotifierType.DISCORD }, + { label: 'Teams', value: NotifierType.TEAMS }, ]} onChange={(value) => { setNotifierType(value); @@ -272,6 +284,13 @@ export function EditNotifierComponent({ setIsUnsaved={setIsUnsaved} /> )} + {notifier?.notifierType === NotifierType.TEAMS && ( + + )}
diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditTeamsNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditTeamsNotifierComponent.tsx new file mode 100644 index 0000000..8d53644 --- /dev/null +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditTeamsNotifierComponent.tsx @@ -0,0 +1,58 @@ +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Input, Tooltip } from 'antd'; +import React from 'react'; + +import type { Notifier } from '../../../../../entity/notifiers'; + +interface Props { + notifier: Notifier; + setNotifier: (notifier: Notifier) => void; + setIsUnsaved: (isUnsaved: boolean) => void; +} + +export function EditTeamsNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { + const value = notifier?.teamsNotifier?.powerAutomateUrl || ''; + + const onChange = (e: React.ChangeEvent) => { + const powerAutomateUrl = e.target.value.trim(); + setNotifier({ + ...notifier, + teamsNotifier: { + ...(notifier.teamsNotifier ?? {}), + powerAutomateUrl, + }, + }); + setIsUnsaved(true); + }; + + return ( + <> +
+
Power Automate URL
+ +
+ +
+ + + + +
+ +
+ 1) In Power Automate create Flow with triggers When an HTTP request is received. +
+ 2) Press Save — you can see URL. Copy urk to this row. +
+ + ); +} diff --git a/frontend/src/features/notifiers/ui/show/ShowNotifierComponent.tsx b/frontend/src/features/notifiers/ui/show/ShowNotifierComponent.tsx index 2a8a5a5..e730ee0 100644 --- a/frontend/src/features/notifiers/ui/show/ShowNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/show/ShowNotifierComponent.tsx @@ -4,6 +4,7 @@ import { getNotifierNameFromType } from '../../../../entity/notifiers/models/get import { ShowDiscordNotifierComponent } from './notifier/ShowDiscordNotifierComponent'; import { ShowEmailNotifierComponent } from './notifier/ShowEmailNotifierComponent'; import { ShowSlackNotifierComponent } from './notifier/ShowSlackNotifierComponent'; +import { ShowTeamsNotifierComponent } from './notifier/ShowTeamsNotifierComponent'; import { ShowTelegramNotifierComponent } from './notifier/ShowTelegramNotifierComponent'; import { ShowWebhookNotifierComponent } from './notifier/ShowWebhookNotifierComponent'; @@ -41,6 +42,10 @@ export function ShowNotifierComponent({ notifier }: Props) { {notifier?.notifierType === NotifierType.DISCORD && ( )} + + {notifier?.notifierType === NotifierType.TEAMS && ( + + )}
); diff --git a/frontend/src/features/notifiers/ui/show/notifier/ShowTeamsNotifierComponent.tsx b/frontend/src/features/notifiers/ui/show/notifier/ShowTeamsNotifierComponent.tsx new file mode 100644 index 0000000..33848d1 --- /dev/null +++ b/frontend/src/features/notifiers/ui/show/notifier/ShowTeamsNotifierComponent.tsx @@ -0,0 +1,42 @@ +import { useState } from 'react'; + +import type { Notifier } from '../../../../../entity/notifiers'; + +interface Props { + notifier: Notifier; +} + +export function ShowTeamsNotifierComponent({ notifier }: Props) { + const url = notifier?.teamsNotifier?.powerAutomateUrl || ''; + const [expanded, setExpanded] = useState(false); + + const MAX = 20; + const isLong = url.length > MAX; + const display = expanded ? url : isLong ? `${url.slice(0, MAX)}…` : url; + + return ( + <> +
+
Power Automate URL:
+
+ {url ? ( + <> + {display} + {isLong && ( + + )} + + ) : ( + '—' + )} +
+
+ + ); +}