Compare commits

...

2 Commits

Author SHA1 Message Date
dedys
0107dab026 FEATURE (notifiers): Add MS Teams notifier 2025-09-08 17:23:47 +03:00
Rostislav Dugin
dee330ed59 FIX (databases): Validate PostgreSQL config always present during DB save 2025-09-05 20:12:34 +03:00
20 changed files with 362 additions and 31 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ postgresus-data/
.env
pgdata/
docker-compose.yml
node_modules/
node_modules/
.idea

View File

@@ -1,6 +1,7 @@
package databases
import (
"errors"
"postgresus-backend/internal/features/databases/databases/postgresql"
"postgresus-backend/internal/storage"
@@ -21,9 +22,12 @@ func (r *DatabaseRepository) Save(database *Database) (*Database, error) {
err := db.Transaction(func(tx *gorm.DB) error {
switch database.Type {
case DatabaseTypePostgres:
if database.Postgresql != nil {
database.Postgresql.DatabaseID = &database.ID
if database.Postgresql == nil {
return errors.New("postgresql configuration is required for PostgreSQL database")
}
// Ensure DatabaseID is always set and never nil
database.Postgresql.DatabaseID = &database.ID
}
if isNew {
@@ -43,17 +47,15 @@ func (r *DatabaseRepository) Save(database *Database) (*Database, error) {
// Save the specific database type
switch database.Type {
case DatabaseTypePostgres:
if database.Postgresql != nil {
database.Postgresql.DatabaseID = &database.ID
if database.Postgresql.ID == uuid.Nil {
database.Postgresql.ID = uuid.New()
if err := tx.Create(database.Postgresql).Error; err != nil {
return err
}
} else {
if err := tx.Save(database.Postgresql).Error; err != nil {
return err
}
database.Postgresql.DatabaseID = &database.ID
if database.Postgresql.ID == uuid.Nil {
database.Postgresql.ID = uuid.New()
if err := tx.Create(database.Postgresql).Error; err != nil {
return err
}
} else {
if err := tx.Save(database.Postgresql).Error; err != nil {
return err
}
}
}

View File

@@ -8,4 +8,5 @@ const (
NotifierTypeWebhook NotifierType = "WEBHOOK"
NotifierTypeSlack NotifierType = "SLACK"
NotifierTypeDiscord NotifierType = "DISCORD"
NotifierTypeTeams NotifierType = "TEAMS"
)

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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(&notifier).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(&notifiers).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
})
}

View File

@@ -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

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#5059C9" d="M10.765 6.875h3.616c.342 0 .619.276.619.617v3.288a2.272 2.272 0 01-2.274 2.27h-.01a2.272 2.272 0 01-2.274-2.27V7.199c0-.179.145-.323.323-.323zM13.21 6.225c.808 0 1.464-.655 1.464-1.462 0-.808-.656-1.463-1.465-1.463s-1.465.655-1.465 1.463c0 .807.656 1.462 1.465 1.462z"/><path fill="#7B83EB" d="M8.651 6.225a2.114 2.114 0 002.117-2.112A2.114 2.114 0 008.65 2a2.114 2.114 0 00-2.116 2.112c0 1.167.947 2.113 2.116 2.113zM11.473 6.875h-5.97a.611.611 0 00-.596.625v3.75A3.669 3.669 0 008.488 15a3.669 3.669 0 003.582-3.75V7.5a.611.611 0 00-.597-.625z"/><path fill="#000000" d="M8.814 6.875v5.255a.598.598 0 01-.596.595H5.193a3.951 3.951 0 01-.287-1.476V7.5a.61.61 0 01.597-.624h3.31z" opacity=".1"/><path fill="#000000" d="M8.488 6.875v5.58a.6.6 0 01-.596.595H5.347a3.22 3.22 0 01-.267-.65 3.951 3.951 0 01-.172-1.15V7.498a.61.61 0 01.596-.624h2.985z" opacity=".2"/><path fill="#000000" d="M8.488 6.875v4.93a.6.6 0 01-.596.595H5.08a3.951 3.951 0 01-.172-1.15V7.498a.61.61 0 01.596-.624h2.985z" opacity=".2"/><path fill="#000000" d="M8.163 6.875v4.93a.6.6 0 01-.596.595H5.079a3.951 3.951 0 01-.172-1.15V7.498a.61.61 0 01.596-.624h2.66z" opacity=".2"/><path fill="#000000" d="M8.814 5.195v1.024c-.055.003-.107.006-.163.006-.055 0-.107-.003-.163-.006A2.115 2.115 0 016.593 4.6h1.625a.598.598 0 01.596.594z" opacity=".1"/><path fill="#000000" d="M8.488 5.52v.699a2.115 2.115 0 01-1.79-1.293h1.195a.598.598 0 01.595.594z" opacity=".2"/><path fill="#000000" d="M8.488 5.52v.699a2.115 2.115 0 01-1.79-1.293h1.195a.598.598 0 01.595.594z" opacity=".2"/><path fill="#000000" d="M8.163 5.52v.647a2.115 2.115 0 01-1.465-1.242h.87a.598.598 0 01.595.595z" opacity=".2"/><path fill="url(#microsoft-teams-color-16__paint0_linear_2372_494)" d="M1.597 4.925h5.969c.33 0 .597.267.597.596v5.958a.596.596 0 01-.597.596h-5.97A.596.596 0 011 11.479V5.521c0-.33.267-.596.597-.596z"/><path fill="#ffffff" d="M6.152 7.193H4.959v3.243h-.76V7.193H3.01v-.63h3.141v.63z"/><defs><linearGradient id="microsoft-teams-color-16__paint0_linear_2372_494" x1="2.244" x2="6.906" y1="4.46" y2="12.548" gradientUnits="userSpaceOnUse"><stop stop-color="#5A62C3"/><stop offset=".5" stop-color="#4D55BD"/><stop offset="1" stop-color="#3940AB"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -4,4 +4,5 @@ export enum NotifierType {
WEBHOOK = 'WEBHOOK',
SLACK = 'SLACK',
DISCORD = 'DISCORD',
TEAMS = 'TEAMS',
}

View File

@@ -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 '';
}

View File

@@ -12,6 +12,8 @@ export const getNotifierNameFromType = (type: NotifierType) => {
return 'Slack';
case NotifierType.DISCORD:
return 'Discord';
case NotifierType.TEAMS:
return 'Teams';
default:
return '';
}

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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<Storage | undefined>();
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 (
<div
className={`mb-3 cursor-pointer rounded p-3 shadow ${selectedDatabaseId === database.id ? 'bg-blue-100' : 'bg-white'}`}
@@ -47,10 +59,25 @@ export const DatabaseCardComponent = ({
<div className="mb flex items-center">
<div className="text-sm text-gray-500">Database type: {databaseType}</div>
<img src={databaseIcon} alt="databaseIcon" className="ml-1 h-4 w-4" />
</div>
{storage && (
<div className="mb-1 text-sm text-gray-500">
<span>Storage: </span>
<span className="inline-flex items-center">
{storage.name}{' '}
{storage.type && (
<img
src={getStorageLogoFromType(storage.type)}
alt="storageIcon"
className="ml-1 h-4 w-4"
/>
)}
</span>
</div>
)}
{database.lastBackupTime && (
<div className="mt-3 mb-1 text-xs text-gray-500">
<span className="font-bold">Last backup</span>

View File

@@ -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 && (
<EditTeamsNotifierComponent
notifier={notifier}
setNotifier={setNotifier}
setIsUnsaved={setIsUnsaved}
/>
)}
</div>
<div className="mt-3 flex">

View File

@@ -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<HTMLInputElement>) => {
const powerAutomateUrl = e.target.value.trim();
setNotifier({
...notifier,
teamsNotifier: {
...(notifier.teamsNotifier ?? {}),
powerAutomateUrl,
},
});
setIsUnsaved(true);
};
return (
<>
<div className="flex items-center">
<div className="w-[130px] min-w-[130px]">Power Automate URL</div>
<div className="w-[250px]">
<Input
value={value}
onChange={onChange}
size="small"
className="w-full"
placeholder="https://prod-00.westeurope.logic.azure.com:443/workflows/....."
/>
</div>
<Tooltip
className="cursor-pointer"
title="HTTP endpoint from your Power Automate flow (When an HTTP request is received)"
>
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
</Tooltip>
</div>
<div className="mb-1 ml-[130px] max-w-[420px] text-xs text-gray-500">
1) In Power Automate create Flow with triggers <i>When an HTTP request is received</i>.
<br />
2) Press <i>Save</i> you can see URL. Copy urk to this row.
</div>
</>
);
}

View File

@@ -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 && (
<ShowDiscordNotifierComponent notifier={notifier} />
)}
{notifier?.notifierType === NotifierType.TEAMS && (
<ShowTeamsNotifierComponent notifier={notifier} />
)}
</div>
</div>
);

View File

@@ -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 (
<>
<div className="flex items-center">
<div className="min-w-[110px]">Power Automate URL: </div>
<div className="w-[250px] break-all">
{url ? (
<>
<span title={url}>{display}</span>
{isLong && (
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="ml-2 text-xs text-blue-600 hover:underline"
>
{expanded ? 'Hide' : 'Show'}
</button>
)}
</>
) : (
'—'
)}
</div>
</div>
</>
);
}