From b7351c220a76232d75a9449a9ceeacd4894e9597 Mon Sep 17 00:00:00 2001
From: mohitdeuex <105265192+mohitdeuex@users.noreply.github.com>
Date: Tue, 30 Aug 2022 11:10:02 +0530
Subject: [PATCH] Added Team Webhook (#6973)
* Added Team Webhook
* Ms Teams UI for setting Page
* Updated Review Comment Changes
* minor changes
* Fix webhook types
* Updated Parser for Teams
* Change MsTeams content and created Justify Card
* Fixed failing tests cases
Co-authored-by: Ashish gupta
Co-authored-by: Ashish Gupta
---
.../events/MSTeamsWebhookPublisher.java | 64 +++++++
.../catalog/jdbi3/WebhookRepository.java | 6 +-
.../slack/SlackWebhookEventPublisher.java | 4 +-
.../catalog/slack/TeamsMessage.java | 34 ++++
.../catalog/util/ChangeEventParser.java | 165 +++++++++++++-----
.../json/schema/entity/events/webhook.json | 9 +-
.../ui/src/assets/svg/ms-teams-grey.svg | 11 ++
.../resources/ui/src/assets/svg/ms-teams.svg | 11 ++
.../src/components/AddWebhook/AddWebhook.tsx | 19 +-
.../ui/src/components/Webhooks/WebhooksV1.tsx | 40 +++--
.../ui/src/components/common/Card/CardV1.tsx | 33 ++++
.../webhook-data-card/WebhookDataCard.tsx | 14 +-
.../ui/src/constants/HelperTextUtil.ts | 6 +
.../constants/globalSettings.constants.tsx | 7 +
.../AddWebhookPage.component.tsx | 10 +-
.../EditWebhookPage.component.tsx | 12 +-
.../MsTeamsPage/MsTeamsPage.component.tsx | 119 +++++++++++++
.../pages/MsTeamsPage/MsTeamsPage.test.tsx | 48 +++++
.../WebhooksPage/WebhooksPageV1.component.tsx | 1 +
.../ui/src/router/GlobalSettingRouter.tsx | 17 ++
.../main/resources/ui/src/utils/SvgUtils.tsx | 12 +-
21 files changed, 559 insertions(+), 83 deletions(-)
create mode 100644 catalog-rest-service/src/main/java/org/openmetadata/catalog/events/MSTeamsWebhookPublisher.java
create mode 100644 catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/TeamsMessage.java
create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams-grey.svg
create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams.svg
create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/Card/CardV1.tsx
create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.component.tsx
create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.test.tsx
diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/MSTeamsWebhookPublisher.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/MSTeamsWebhookPublisher.java
new file mode 100644
index 00000000000..17be15513e4
--- /dev/null
+++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/events/MSTeamsWebhookPublisher.java
@@ -0,0 +1,64 @@
+package org.openmetadata.catalog.events;
+
+import java.util.concurrent.TimeUnit;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import lombok.extern.slf4j.Slf4j;
+import org.openmetadata.catalog.events.errors.EventPublisherException;
+import org.openmetadata.catalog.jdbi3.CollectionDAO;
+import org.openmetadata.catalog.resources.events.EventResource;
+import org.openmetadata.catalog.slack.SlackRetriableException;
+import org.openmetadata.catalog.slack.TeamsMessage;
+import org.openmetadata.catalog.type.ChangeEvent;
+import org.openmetadata.catalog.type.Webhook;
+import org.openmetadata.catalog.util.ChangeEventParser;
+
+@Slf4j
+public class MSTeamsWebhookPublisher extends WebhookPublisher {
+ private final Invocation.Builder target;
+ private final Client client;
+
+ public MSTeamsWebhookPublisher(Webhook webhook, CollectionDAO dao) {
+ super(webhook, dao);
+ String msTeamsWebhookURL = webhook.getEndpoint().toString();
+ ClientBuilder clientBuilder = ClientBuilder.newBuilder();
+ clientBuilder.connectTimeout(webhook.getTimeout(), TimeUnit.SECONDS);
+ clientBuilder.readTimeout(webhook.getReadTimeout(), TimeUnit.SECONDS);
+ client = clientBuilder.build();
+ target = client.target(msTeamsWebhookURL).request();
+ }
+
+ @Override
+ public void onStart() {
+ LOG.info("Slack Webhook Publisher Started");
+ }
+
+ @Override
+ public void onShutdown() {
+ if (client != null) {
+ client.close();
+ }
+ }
+
+ @Override
+ public void publish(EventResource.ChangeEventList events) throws EventPublisherException {
+ for (ChangeEvent event : events.getData()) {
+ try {
+ TeamsMessage teamsMessage = ChangeEventParser.buildTeamsMessage(event);
+ Response response =
+ target.post(javax.ws.rs.client.Entity.entity(teamsMessage, MediaType.APPLICATION_JSON_TYPE));
+ if (response.getStatus() >= 300 && response.getStatus() < 400) {
+ throw new EventPublisherException(
+ "Slack webhook callback is getting redirected. " + "Please check your configuration");
+ } else if (response.getStatus() >= 300 && response.getStatus() < 600) {
+ throw new SlackRetriableException(response.getStatusInfo().getReasonPhrase());
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to publish event {} to slack due to {} ", event, e.getMessage());
+ }
+ }
+ }
+}
diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/WebhookRepository.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/WebhookRepository.java
index dc337cd9675..9ae4cd67e6e 100644
--- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/WebhookRepository.java
+++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/jdbi3/WebhookRepository.java
@@ -28,9 +28,9 @@ import lombok.extern.slf4j.Slf4j;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.events.EventPubSub;
import org.openmetadata.catalog.events.EventPubSub.ChangeEventHolder;
+import org.openmetadata.catalog.events.MSTeamsWebhookPublisher;
import org.openmetadata.catalog.events.WebhookPublisher;
import org.openmetadata.catalog.filter.EventFilter;
-import org.openmetadata.catalog.kafka.KafkaWebhookEventPublisher;
import org.openmetadata.catalog.resources.events.WebhookResource;
import org.openmetadata.catalog.slack.SlackWebhookEventPublisher;
import org.openmetadata.catalog.type.Webhook;
@@ -90,8 +90,8 @@ public class WebhookRepository extends EntityRepository {
WebhookPublisher publisher;
if (webhook.getWebhookType() == WebhookType.slack) {
publisher = new SlackWebhookEventPublisher(webhook, daoCollection);
- } else if (webhook.getWebhookType() == WebhookType.kafka) {
- publisher = new KafkaWebhookEventPublisher(webhook, daoCollection);
+ } else if (webhook.getWebhookType() == WebhookType.msteams) {
+ publisher = new MSTeamsWebhookPublisher(webhook, daoCollection);
} else {
publisher = new WebhookPublisher(webhook, daoCollection);
}
diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/SlackWebhookEventPublisher.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/SlackWebhookEventPublisher.java
index 08770600251..03933d0ef85 100644
--- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/SlackWebhookEventPublisher.java
+++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/SlackWebhookEventPublisher.java
@@ -24,8 +24,8 @@ public class SlackWebhookEventPublisher extends WebhookPublisher {
super(webhook, dao);
String slackWebhookURL = webhook.getEndpoint().toString();
ClientBuilder clientBuilder = ClientBuilder.newBuilder();
- clientBuilder.connectTimeout(10, TimeUnit.SECONDS);
- clientBuilder.readTimeout(12, TimeUnit.SECONDS);
+ clientBuilder.connectTimeout(webhook.getTimeout(), TimeUnit.SECONDS);
+ clientBuilder.readTimeout(webhook.getReadTimeout(), TimeUnit.SECONDS);
client = clientBuilder.build();
target = client.target(slackWebhookURL).request();
}
diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/TeamsMessage.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/TeamsMessage.java
new file mode 100644
index 00000000000..9c712453278
--- /dev/null
+++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/slack/TeamsMessage.java
@@ -0,0 +1,34 @@
+package org.openmetadata.catalog.slack;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@Getter
+@Setter
+public class TeamsMessage {
+ @Getter
+ @Setter
+ public static class Section {
+ @JsonProperty("activityTitle")
+ public String activityTitle;
+
+ @JsonProperty("activityText")
+ public String activityText;
+ }
+
+ @JsonProperty("@type")
+ public String type = "MessageCard";
+
+ @JsonProperty("@context")
+ public String context = "http://schema.org/extensions";
+
+ @JsonProperty("summary")
+ public String summary;
+
+ @JsonProperty("sections")
+ public List sections;
+}
diff --git a/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/ChangeEventParser.java b/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/ChangeEventParser.java
index e361f85295b..fbc471f120a 100644
--- a/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/ChangeEventParser.java
+++ b/catalog-rest-service/src/main/java/org/openmetadata/catalog/util/ChangeEventParser.java
@@ -41,6 +41,7 @@ import org.openmetadata.catalog.EntityInterface;
import org.openmetadata.catalog.resources.feeds.MessageParser.EntityLink;
import org.openmetadata.catalog.slack.SlackAttachment;
import org.openmetadata.catalog.slack.SlackMessage;
+import org.openmetadata.catalog.slack.TeamsMessage;
import org.openmetadata.catalog.type.ChangeDescription;
import org.openmetadata.catalog.type.ChangeEvent;
import org.openmetadata.catalog.type.EntityReference;
@@ -67,17 +68,104 @@ public final class ChangeEventParser {
public enum PUBLISH_TO {
FEED,
- SLACK
+ SLACK,
+ TEAMS
}
- public static String getEntityUrl(ChangeEvent event) {
+ public static String getBold(PUBLISH_TO publishTo) {
+ switch (publishTo) {
+ case FEED:
+ case TEAMS:
+ // TEAMS and FEED bold formatting is same
+ return FEED_BOLD;
+ case SLACK:
+ return SLACK_BOLD;
+ default:
+ return "INVALID";
+ }
+ }
+
+ public static String getLineBreak(PUBLISH_TO publishTo) {
+ switch (publishTo) {
+ case FEED:
+ case TEAMS:
+ // TEAMS and FEED bold formatting is same
+ return FEED_LINE_BREAK;
+ case SLACK:
+ return SLACK_LINE_BREAK;
+ default:
+ return "INVALID";
+ }
+ }
+
+ public static String getAddMarker(PUBLISH_TO publishTo) {
+ switch (publishTo) {
+ case FEED:
+ return FEED_SPAN_ADD;
+ case TEAMS:
+ // TEAMS and FEED bold formatting is same
+ return "**";
+ case SLACK:
+ return "*";
+ default:
+ return "INVALID";
+ }
+ }
+
+ public static String getAddMarkerClose(PUBLISH_TO publishTo) {
+ switch (publishTo) {
+ case FEED:
+ return FEED_SPAN_CLOSE;
+ case TEAMS:
+ // TEAMS and FEED bold formatting is same
+ return "** ";
+ case SLACK:
+ return "* ";
+ default:
+ return "INVALID";
+ }
+ }
+
+ public static String getRemoveMarker(PUBLISH_TO publishTo) {
+ switch (publishTo) {
+ case FEED:
+ return FEED_SPAN_REMOVE;
+ case TEAMS:
+ // TEAMS and FEED bold formatting is same
+ return "~~";
+ case SLACK:
+ return "~";
+ default:
+ return "INVALID";
+ }
+ }
+
+ public static String getRemoveMarkerClose(PUBLISH_TO publishTo) {
+ switch (publishTo) {
+ case FEED:
+ return FEED_SPAN_CLOSE;
+ case TEAMS:
+ // TEAMS and FEED bold formatting is same
+ return "~~ ";
+ case SLACK:
+ return "~ ";
+ default:
+ return "INVALID";
+ }
+ }
+
+ public static String getEntityUrl(PUBLISH_TO publishTo, ChangeEvent event) {
EntityInterface entity = (EntityInterface) event.getEntity();
URI urlInstance = entity.getHref();
String fqn = event.getEntityFullyQualifiedName();
if (Objects.nonNull(urlInstance)) {
String scheme = urlInstance.getScheme();
String host = urlInstance.getHost();
- return String.format("<%s://%s/%s/%s|%s>", scheme, host, event.getEntityType(), fqn, fqn);
+ if (publishTo == PUBLISH_TO.SLACK) {
+ return String.format("<%s://%s/%s/%s|%s>", scheme, host, event.getEntityType(), fqn, fqn);
+ } else if (publishTo == PUBLISH_TO.TEAMS) {
+ return String.format("[%s](%s://%s/%s/%s)", fqn, scheme, host, event.getEntityType(), fqn);
+ }
}
return urlInstance.toString();
}
@@ -87,7 +175,7 @@ public final class ChangeEventParser {
slackMessage.setUsername(event.getUserName());
if (event.getEntity() != null) {
String headerTxt = "%s posted on " + event.getEntityType() + " %s";
- String headerText = String.format(headerTxt, event.getUserName(), getEntityUrl(event));
+ String headerText = String.format(headerTxt, event.getUserName(), getEntityUrl(PUBLISH_TO.SLACK, event));
slackMessage.setText(headerText);
}
Map messages =
@@ -105,6 +193,28 @@ public final class ChangeEventParser {
return slackMessage;
}
+ public static TeamsMessage buildTeamsMessage(ChangeEvent event) {
+ TeamsMessage teamsMessage = new TeamsMessage();
+ teamsMessage.setSummary("Change Event From OMD");
+ TeamsMessage.Section teamsSections = new TeamsMessage.Section();
+ if (event.getEntity() != null) {
+ String headerTxt = "%s posted on " + event.getEntityType() + " %s";
+ String headerText = String.format(headerTxt, event.getUserName(), getEntityUrl(PUBLISH_TO.TEAMS, event));
+ teamsSections.setActivityTitle(headerText);
+ }
+ Map messages =
+ getFormattedMessages(PUBLISH_TO.TEAMS, event.getChangeDescription(), (EntityInterface) event.getEntity());
+ List attachmentList = new ArrayList<>();
+ for (var entry : messages.entrySet()) {
+ TeamsMessage.Section section = new TeamsMessage.Section();
+ section.setActivityTitle(teamsSections.getActivityTitle());
+ section.setActivityText(entry.getValue());
+ attachmentList.add(section);
+ }
+ teamsMessage.setSections(attachmentList);
+ return teamsMessage;
+ }
+
public static Map getFormattedMessages(
PUBLISH_TO publishTo, ChangeDescription changeDescription, EntityInterface entity) {
// Store a map of entityLink -> message
@@ -278,16 +388,9 @@ public final class ChangeEventParser {
String fieldValue = getFieldValue(newFieldValue);
if (Entity.FIELD_FOLLOWERS.equals(updatedField)) {
message =
- String.format(
- ("Followed " + (publishTo == PUBLISH_TO.FEED ? FEED_BOLD : SLACK_BOLD) + " `%s`"),
- link.getEntityType(),
- link.getEntityFQN());
+ String.format(("Followed " + getBold(publishTo) + " `%s`"), link.getEntityType(), link.getEntityFQN());
} else if (fieldValue != null && !fieldValue.isEmpty()) {
- message =
- String.format(
- ("Added " + (publishTo == PUBLISH_TO.FEED ? FEED_BOLD : SLACK_BOLD) + ": `%s`"),
- updatedField,
- fieldValue);
+ message = String.format(("Added " + getBold(publishTo) + ": `%s`"), updatedField, fieldValue);
}
break;
case UPDATE:
@@ -297,7 +400,7 @@ public final class ChangeEventParser {
if (Entity.FIELD_FOLLOWERS.equals(updatedField)) {
message = String.format("Unfollowed %s `%s`", link.getEntityType(), link.getEntityFQN());
} else {
- message = String.format(("Deleted " + (publishTo == PUBLISH_TO.FEED ? FEED_BOLD : SLACK_BOLD)), updatedField);
+ message = String.format(("Deleted " + getBold(publishTo)), updatedField);
}
break;
default:
@@ -313,7 +416,7 @@ public final class ChangeEventParser {
if (nullOrEmpty(diff)) {
return StringUtils.EMPTY;
} else {
- String field = String.format("Updated %s: %s", (publishTo == PUBLISH_TO.FEED ? FEED_BOLD : SLACK_BOLD), diff);
+ String field = String.format("Updated %s: %s", getBold(publishTo), diff);
return String.format(field, updatedField);
}
}
@@ -330,17 +433,12 @@ public final class ChangeEventParser {
"%s: %s", key, getPlaintextDiff(publishTo, oldJson.get(key).toString(), newJson.get(key).toString())));
}
}
- String updates = String.join((publishTo == PUBLISH_TO.FEED ? FEED_LINE_BREAK : SLACK_LINE_BREAK), labels);
+ String updates = String.join(getLineBreak(publishTo), labels);
// Include name of the field if the json contains "name" key
if (newJson.containsKey("name")) {
updatedField = String.format("%s.%s", updatedField, newJson.getString("name"));
}
- String format =
- String.format(
- "Updated %s:%s%s",
- publishTo == PUBLISH_TO.FEED ? FEED_BOLD : SLACK_BOLD,
- publishTo == PUBLISH_TO.FEED ? FEED_LINE_BREAK : SLACK_LINE_BREAK,
- updates);
+ String format = String.format("Updated %s:%s%s", getBold(publishTo), getLineBreak(publishTo), updates);
return String.format(format, updatedField);
}
@@ -351,9 +449,7 @@ public final class ChangeEventParser {
}
if (oldValue == null || oldValue.toString().isEmpty()) {
- String format =
- String.format(
- "Updated %s to %s", publishTo == PUBLISH_TO.FEED ? FEED_BOLD : SLACK_BOLD, getFieldValue(newValue));
+ String format = String.format("Updated %s to %s", getBold(publishTo), getFieldValue(newValue));
return String.format(format, updatedField);
} else if (updatedField.contains("tags") || updatedField.contains(FIELD_OWNER)) {
return getPlainTextUpdateMessage(publishTo, updatedField, getFieldValue(oldValue), getFieldValue(newValue));
@@ -426,21 +522,10 @@ public final class ChangeEventParser {
// Replace them with html tags to render nicely in the UI
// Example: This is a test sentenceline
// This is a test sentence line
- String spanAdd;
- String spanAddClose;
- String spanRemove;
- String spanRemoveClose;
- if (publishTo == PUBLISH_TO.FEED) {
- spanAdd = FEED_SPAN_ADD;
- spanAddClose = FEED_SPAN_CLOSE;
- spanRemove = FEED_SPAN_REMOVE;
- spanRemoveClose = FEED_SPAN_CLOSE;
- } else {
- spanAdd = "*";
- spanAddClose = "* ";
- spanRemove = "~";
- spanRemoveClose = "~ ";
- }
+ String spanAdd = getAddMarker(publishTo);
+ String spanAddClose = getAddMarkerClose(publishTo);
+ String spanRemove = getRemoveMarker(publishTo);
+ String spanRemoveClose = getRemoveMarkerClose(publishTo);
if (diff != null) {
diff = replaceMarkers(diff, addMarker, spanAdd, spanAddClose);
diff = replaceMarkers(diff, removeMarker, spanRemove, spanRemoveClose);
diff --git a/catalog-rest-service/src/main/resources/json/schema/entity/events/webhook.json b/catalog-rest-service/src/main/resources/json/schema/entity/events/webhook.json
index 07f633de851..387069dbc3d 100644
--- a/catalog-rest-service/src/main/resources/json/schema/entity/events/webhook.json
+++ b/catalog-rest-service/src/main/resources/json/schema/entity/events/webhook.json
@@ -12,7 +12,7 @@
"type": "string",
"javaType": "org.openmetadata.catalog.type.WebhookType",
"default": "generic",
- "enum": ["slack", "generic", "kafka"],
+ "enum": ["slack", "generic", "msteams"],
"javaEnums": [
{
"name": "slack"
@@ -21,7 +21,7 @@
"name": "generic"
},
{
- "name": "kafka"
+ "name": "msteams"
}
]
}
@@ -77,6 +77,11 @@
"type": "integer",
"default": 10
},
+ "readTimeout": {
+ "description": "Read timeout in seconds. (Default 12s).",
+ "type": "integer",
+ "default": 12
+ },
"enabled": {
"description": "When set to `true`, the webhook event notification is enabled. Set it to `false` to disable the subscription. (Default `true`).",
"type": "boolean",
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams-grey.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams-grey.svg
new file mode 100644
index 00000000000..a2d975bae3e
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams-grey.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams.svg
new file mode 100644
index 00000000000..e1c41e4a2a8
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ms-teams.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx
index e44fc20b2ad..d3ba1f4a6e0 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/AddWebhook/AddWebhook.tsx
@@ -32,6 +32,7 @@ import {
GlobalSettingsMenuCategory,
} from '../../constants/globalSettings.constants';
import {
+ CONFIGURE_MS_TEAMS_TEXT,
CONFIGURE_SLACK_TEXT,
CONFIGURE_WEBHOOK_TEXT,
NO_PERMISSION_FOR_ACTION,
@@ -57,6 +58,7 @@ import { getSettingPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { Button } from '../buttons/Button/Button';
import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton';
+import CardV1 from '../common/Card/CardV1';
import RichTextEditor from '../common/rich-text-editor/RichTextEditor';
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
import PageLayout from '../containers/PageLayout';
@@ -71,6 +73,12 @@ import {
EVENT_FILTER_FORM_INITIAL_VALUE,
} from './WebhookConstants';
+const CONFIGURE_TEXT: { [key: string]: string } = {
+ msteams: CONFIGURE_MS_TEAMS_TEXT,
+ slack: CONFIGURE_SLACK_TEXT,
+ generic: CONFIGURE_WEBHOOK_TEXT,
+};
+
const Field = ({ children }: { children: React.ReactNode }) => {
return {children}
;
};
@@ -374,12 +382,11 @@ const AddWebhook: FunctionComponent = ({
const fetchRightPanel = useCallback(() => {
return (
-
Configure Webhooks
-
- {webhookType === WebhookType.Slack
- ? CONFIGURE_SLACK_TEXT
- : CONFIGURE_WEBHOOK_TEXT}
-
+
);
}, [webhookType]);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx
index 047263ec853..6ea670a56f4 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Webhooks/WebhooksV1.tsx
@@ -11,12 +11,13 @@
* limitations under the License.
*/
-import { Card, Col, Row, Select, Space, Tooltip } from 'antd';
+import { Col, Row, Select, Space, Tooltip } from 'antd';
import classNames from 'classnames';
import { isEmpty, isNil } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { PAGE_SIZE } from '../../constants/constants';
import {
+ MS_TEAMS_LISTING_TEXT,
NO_PERMISSION_FOR_ACTION,
SLACK_LISTING_TEXT,
WEBHOOK_LISTING_TEXT,
@@ -27,6 +28,7 @@ import { Operation } from '../../generated/entity/policies/policy';
import { checkPermission } from '../../utils/PermissionsUtils';
import { statuses } from '../AddWebhook/WebhookConstants';
import { Button } from '../buttons/Button/Button';
+import CardV1 from '../common/Card/CardV1';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import NextPrevious from '../common/next-previous/NextPrevious';
import WebhookDataCard from '../common/webhook-data-card/WebhookDataCard';
@@ -36,9 +38,21 @@ import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interfa
import { WebhooksV1Props } from './WebhooksV1.interface';
import './webhookV1.less';
+const LISTING_TEXT: { [key: string]: string } = {
+ msteams: MS_TEAMS_LISTING_TEXT,
+ slack: SLACK_LISTING_TEXT,
+ generic: WEBHOOK_LISTING_TEXT,
+};
+
+const WEBHOOKS_INTEGRATION: { [key: string]: string } = {
+ msteams: 'MS Teams',
+ slack: 'Slack',
+ generic: 'Webhook',
+};
+
const WebhooksV1: FC = ({
data = [],
- webhookType,
+ webhookType = WebhookType.Generic,
paging,
selectedStatus = [],
onAddWebhook,
@@ -68,16 +82,9 @@ const WebhooksV1: FC = ({
const rightPanel = useMemo(() => {
return (
-
-
- {webhookType === WebhookType.Slack
- ? SLACK_LISTING_TEXT
- : WEBHOOK_LISTING_TEXT}
-
-
+
+
+
);
}, []);
@@ -100,7 +107,7 @@ const WebhooksV1: FC = ({
theme="primary"
variant="contained"
onClick={onAddWebhook}>
- Add {webhookType === WebhookType.Slack ? 'Slack' : 'Webhook'}
+ Add {WEBHOOKS_INTEGRATION[webhookType]}
@@ -115,7 +122,9 @@ const WebhooksV1: FC = ({
}, [data, selectedStatus]);
if (data.length === 0) {
- return fetchErrorPlaceHolder('No webhooks found');
+ return fetchErrorPlaceHolder(
+ `No ${WEBHOOKS_INTEGRATION[webhookType]} found`
+ );
}
return (
@@ -155,8 +164,7 @@ const WebhooksV1: FC = ({
theme="primary"
variant="contained"
onClick={onAddWebhook}>
- Add{' '}
- {webhookType === WebhookType.Slack ? 'Slack' : 'Webhook'}
+ Add {WEBHOOKS_INTEGRATION[webhookType]}
)}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Card/CardV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Card/CardV1.tsx
new file mode 100644
index 00000000000..4cde25eda1a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Card/CardV1.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Card } from 'antd';
+import { lowerCase } from 'lodash';
+import React from 'react';
+
+interface CardProps {
+ description: string;
+ id: string;
+ heading?: string;
+}
+
+const CardV1 = ({ description, id, heading }: CardProps) => {
+ return (
+
+ {heading ? {heading} : ''}
+ {description}
+
+ );
+};
+
+export default CardV1;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/webhook-data-card/WebhookDataCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/webhook-data-card/WebhookDataCard.tsx
index 628f99e4d66..d2de68fb036 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/webhook-data-card/WebhookDataCard.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/webhook-data-card/WebhookDataCard.tsx
@@ -26,11 +26,17 @@ type Props = {
onClick?: (name: string) => void;
};
+const ICON: { [key: string]: string } = {
+ generic: Icons.WEBHOOK,
+ msteams: Icons.MSTEAMS_GREY,
+ slack: Icons.SLACK_GREY,
+};
+
const WebhookDataCard: FunctionComponent = ({
name,
description,
endpoint,
- type,
+ type = WebhookType.Generic,
status = Status.Disabled,
onClick,
}: Props) => {
@@ -44,11 +50,7 @@ const WebhookDataCard: FunctionComponent = ({
data-testid="webhook-data-card">
-
+
,
},
+ {
+ label: 'MS Teams',
+ isProtected: true,
+ icon: ,
+ },
{
label: 'Bots',
isProtected: true,
@@ -179,4 +185,5 @@ export enum GlobalSettingOptions {
SLACK = 'slack',
BOTS = 'bots',
TABLES = 'tables',
+ MSTEAMS = 'msteams',
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddWebhookPage/AddWebhookPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddWebhookPage/AddWebhookPage.component.tsx
index 799a70f8a95..3a9a8b47e2e 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/AddWebhookPage/AddWebhookPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddWebhookPage/AddWebhookPage.component.tsx
@@ -33,6 +33,12 @@ import jsonData from '../../jsons/en';
import { getSettingPath } from '../../utils/RouterUtils';
import { showErrorToast } from '../../utils/ToastUtils';
+const HEADER_TEXT_WEBHOOK: { [key: string]: string } = {
+ msteams: 'MS Teams',
+ slack: 'Slack',
+ generic: 'Webhook',
+};
+
const AddWebhookPage: FunctionComponent = () => {
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
@@ -89,9 +95,7 @@ const AddWebhookPage: FunctionComponent = () => {
{
const { webhookName } = useParams<{ [key: string]: string }>();
const { isAdminUser } = useAuth();
@@ -126,9 +132,7 @@ const EditWebhookPage: FunctionComponent = () => {
data={webhookData}
deleteState={deleteStatus}
header={
- webhookData?.webhookType === WebhookType.Slack
- ? 'Edit Slack'
- : 'Edit Webhook'
+ EDIT_HEADER_WEBHOOKS_TITLE[webhookData?.webhookType || 'generic']
}
mode={FormSubmitType.EDIT}
saveState={status}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.component.tsx
new file mode 100644
index 00000000000..705fe722cb9
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.component.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2022 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AxiosError } from 'axios';
+import React, { useEffect, useState } from 'react';
+import { useHistory } from 'react-router-dom';
+import { getWebhooks } from '../../axiosAPIs/webhookAPI';
+import PageContainerV1 from '../../components/containers/PageContainerV1';
+import Loader from '../../components/Loader/Loader';
+import WebhooksV1 from '../../components/Webhooks/WebhooksV1';
+import {
+ getAddWebhookPath,
+ getEditWebhookPath,
+ pagingObject,
+} from '../../constants/constants';
+import {
+ Status,
+ Webhook,
+ WebhookType,
+} from '../../generated/entity/events/webhook';
+import { Paging } from '../../generated/type/paging';
+import jsonData from '../../jsons/en';
+import { showErrorToast } from '../../utils/ToastUtils';
+
+const MsTeamsPage = () => {
+ const history = useHistory();
+ const [isLoading, setIsLoading] = useState(true);
+ const [paging, setPaging] = useState(pagingObject);
+ const [data, setData] = useState>([]);
+ const [selectedStatus, setSelectedStatus] = useState([]);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const fetchData = (paging?: string) => {
+ setIsLoading(true);
+ getWebhooks(paging)
+ .then((res) => {
+ if (res.data) {
+ const genericWebhooks = res.data.filter(
+ (d) => d.webhookType === WebhookType.Msteams
+ );
+ setData(genericWebhooks);
+ setPaging(res.paging);
+ } else {
+ setData([]);
+ setPaging(pagingObject);
+
+ throw jsonData['api-error-messages']['unexpected-server-response'];
+ }
+ })
+ .catch((err: AxiosError) => {
+ showErrorToast(
+ err,
+ jsonData['api-error-messages']['fetch-webhook-error']
+ );
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ };
+
+ const handlePageChange = (
+ cursorType: string | number,
+ activePage?: number
+ ) => {
+ const pagingString = `&${cursorType}=${
+ paging[cursorType as keyof typeof paging]
+ }`;
+ fetchData(pagingString);
+ setCurrentPage(activePage ?? 1);
+ };
+
+ const handleStatusFilter = (status: Status[]) => {
+ setSelectedStatus(status);
+ };
+
+ const handleAddWebhook = () => {
+ history.push(getAddWebhookPath(WebhookType.Msteams));
+ };
+
+ const handleClickWebhook = (name: string) => {
+ history.push(getEditWebhookPath(name));
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ return (
+
+ {!isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default MsTeamsPage;
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.test.tsx
new file mode 100644
index 00000000000..84a5895196d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/MsTeamsPage/MsTeamsPage.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 Collate
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { findByTestId, findByText, render } from '@testing-library/react';
+import React, { ReactNode } from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { getWebhooks } from '../../axiosAPIs/webhookAPI';
+import MsTeamsPage from './MsTeamsPage.component';
+
+jest.mock('../../components/containers/PageContainerV1', () => {
+ return jest
+ .fn()
+ .mockImplementation(({ children }: { children: ReactNode }) => (
+ {children}
+ ));
+});
+
+jest.mock('../../components/Webhooks/WebhooksV1', () => {
+ return jest.fn().mockImplementation(() => <>testWebhookV1>);
+});
+
+jest.mock('../../axiosAPIs/webhookAPI.ts', () => ({
+ getWebhooks: jest.fn().mockImplementation(() => Promise.resolve()),
+}));
+
+describe('Test MS Teams page Component', () => {
+ it('should load WebhookV1 component on API success', async () => {
+ const { container } = render( , {
+ wrapper: MemoryRouter,
+ });
+ const PageContainerV1 = await findByTestId(container, 'PageContainerV1');
+ const webhookComponent = await findByText(container, /testWebhookV1/);
+
+ expect(PageContainerV1).toBeInTheDocument();
+ expect(webhookComponent).toBeInTheDocument();
+ expect(getWebhooks).toBeCalledTimes(1);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/WebhooksPage/WebhooksPageV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/WebhooksPage/WebhooksPageV1.component.tsx
index 65a50a50ce6..fdcf87b5832 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/WebhooksPage/WebhooksPageV1.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/WebhooksPage/WebhooksPageV1.component.tsx
@@ -104,6 +104,7 @@ const WebhooksPageV1 = () => {
data={data}
paging={paging}
selectedStatus={selectedStatus}
+ webhookType={WebhookType.Generic}
onAddWebhook={handleAddWebhook}
onClickWebhook={handleClickWebhook}
onPageChange={handlePageChange}
diff --git a/openmetadata-ui/src/main/resources/ui/src/router/GlobalSettingRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/router/GlobalSettingRouter.tsx
index 1a1b8245bd8..fa09baed65d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/router/GlobalSettingRouter.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/router/GlobalSettingRouter.tsx
@@ -66,6 +66,9 @@ const SlackSettingsPage = withSuspenseFallback(
() => import('../pages/SlackSettingsPage/SlackSettingsPage.component')
)
);
+const MsTeamsPage = withSuspenseFallback(
+ React.lazy(() => import('../pages/MsTeamsPage/MsTeamsPage.component'))
+);
const GlobalSettingRouter = () => {
const { permissions } = usePermissionProvider();
@@ -211,6 +214,20 @@ const GlobalSettingRouter = () => {
)}
/>
+
+
= ({
@@ -993,12 +997,18 @@ const SVGIcons: FunctionComponent = ({
IconComponent = IconInfoSecondary;
break;
+ case Icons.MSTEAMS:
+ IconComponent = IconMSTeams;
+ break;
+ case Icons.MSTEAMS_GREY:
+ IconComponent = IconMSTeamsGrey;
+
+ break;
case Icons.DELETE_COLORED:
IconComponent = IconDeleteColored;
break;
-
default:
IconComponent = null;