diff --git a/VERSION b/VERSION index 8298bb08b2..a8ab6c9666 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.43.0 +0.44.0 diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java index 1c846c2cd1..280303aa4f 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/WebhooksController.java @@ -45,6 +45,7 @@ public ResponseEntity subscribe(@RequestBody @Valid WebhookSubscribePayload p final UUID id = Optional.ofNullable(payload.getId()).orElse(UUID.randomUUID()); final Webhook webhook = Webhook.newBuilder() .setId(id.toString()) + .setName(payload.getName()) .setEvents(payload.getEvents().stream().map(EventType::getEventType).collect(Collectors.toList())) .setEndpoint(payload.getUrl().toString()) .setStatus(Status.Subscribed) @@ -78,13 +79,13 @@ public ResponseEntity update(@RequestBody @Valid WebhookUpdatePayload payload if (webhook == null) { return ResponseEntity.notFound().build(); } - if (webhook.getStatus().equals(Status.Unsubscribed)) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(fromWebhook(webhook)); - } if (payload.getUrl() != null) { webhook.setEndpoint(payload.getUrl().toString()); } + if (payload.getName() != null) { + webhook.setName(payload.getName().toString()); + } if (payload.getEvents() != null) { webhook.setEvents(payload.getEvents().stream().map(EventType::getEventType).collect(Collectors.toList())); } @@ -94,6 +95,14 @@ public ResponseEntity update(@RequestBody @Valid WebhookUpdatePayload payload if (payload.getSignatureKey() != null) { webhook.setSignKey(payload.getSignatureKey()); } + if (payload.getStatus() != null) { + if (Status.Subscribed.toString().equals(payload.getStatus().toString())) { + webhook.setStatus(Status.Subscribed); + } + if (Status.Unsubscribed.toString().equals(payload.getStatus().toString())) { + webhook.setStatus(Status.Unsubscribed); + } + } try { stores.storeWebhook(webhook); @@ -138,7 +147,6 @@ public ResponseEntity webhookInfo(@RequestBody @Valid We @PostMapping("/webhooks.list") public ResponseEntity webhookList() { final List webhooks = stores.getWebhooks().stream() - .filter(((webhook) -> webhook.getStatus().equals(Status.Subscribed))) .map(this::fromWebhookList).collect(Collectors.toList()); return ResponseEntity.status(HttpStatus.OK).body(new WebhookListResponsePayload(webhooks)); } @@ -146,6 +154,7 @@ public ResponseEntity webhookList() { private WebhookResponsePayload fromWebhook(Webhook webhook) { return WebhookResponsePayload.builder() .id(webhook.getId()) + .name(webhook.getName()) .events(webhook.getEvents()) .headers(webhook.getHeaders()) .status(webhook.getStatus().toString()) @@ -156,8 +165,10 @@ private WebhookResponsePayload fromWebhook(Webhook webhook) { private WebhookListPayload fromWebhookList(Webhook webhook) { return WebhookListPayload.builder() .id(webhook.getId()) + .name(webhook.getName()) .events(webhook.getEvents()) .headers(webhook.getHeaders()) + .status(webhook.getStatus().toString()) .url(webhook.getEndpoint()) .build(); } diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookListPayload.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookListPayload.java index e1d0c16b72..1fd84d357d 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookListPayload.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookListPayload.java @@ -14,4 +14,5 @@ public class WebhookListPayload { private String url; private Map headers; private List events; + private String status; } diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java index b8f191a481..5725eb2b1b 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookSubscribePayload.java @@ -20,6 +20,7 @@ public class WebhookSubscribePayload { private UUID id; @NotNull private URL url; + private String name; private Map headers = new HashMap<>(); private List events = new ArrayList<>(); private String signatureKey; diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUpdatePayload.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUpdatePayload.java index 7928ba5eac..f097932fef 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUpdatePayload.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/WebhookUpdatePayload.java @@ -18,9 +18,11 @@ @AllArgsConstructor public class WebhookUpdatePayload { private URL url; + private String name; private Map headers = new HashMap<>(); private List events = new ArrayList<>(); private String signatureKey; + private String status; @NotNull private UUID id; } diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java index 00600ad889..b88d17c816 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java @@ -30,7 +30,6 @@ public ClientConfigController(ServiceDiscovery serviceDiscovery, PrincipalAccess this.tagConfig = new ObjectMapper().readTree(tagConfigResource); try (InputStream is = version.getInputStream()) { clusterVersion = StreamUtils.copyToString(is, StandardCharsets.UTF_8); - ; } } diff --git a/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java b/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java index dd4fe7d8fe..84df53b169 100644 --- a/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java +++ b/backend/api/admin/src/test/java/co/airy/core/api/admin/WebhooksControllerTest.java @@ -83,15 +83,16 @@ public void canManageWebhook() throws Exception { webTestHelper.post("/webhooks.info", infoPayload).andExpect(status().isNotFound()); final String url = "http://example.org/webhook"; + final String name = "webhook name"; final String xAuthHeader = "auth token"; final EventType subscribeEvent = EventType.MESSAGE_CREATED; final EventType newSubscribeEvent = EventType.MESSAGE_UPDATED; - final String subscribePayload = String.format("{\"id\":\"%s\",\"url\":\"%s\",\"headers\":{\"X-Auth\":\"%s\"},\"events\":[\"%s\"]}", - webhookId, url, xAuthHeader, subscribeEvent.getEventType()); + final String subscribePayload = String.format("{\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"headers\":{\"X-Auth\":\"%s\"},\"events\":[\"%s\"]}", + webhookId, name, url, xAuthHeader, subscribeEvent.getEventType()); - final String updatePayload = String.format("{\"id\":\"%s\",\"url\":\"%s\",\"headers\":{\"X-Auth\":\"%s\"},\"events\":[\"%s\", \"%s\"]}", - webhookId, url, xAuthHeader, subscribeEvent.getEventType(), newSubscribeEvent.getEventType()); + final String updatePayload = String.format("{\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"headers\":{\"X-Auth\":\"%s\"},\"events\":[\"%s\", \"%s\"]}", + webhookId, name, url, xAuthHeader, subscribeEvent.getEventType(), newSubscribeEvent.getEventType()); when(serviceDiscovery.getComponent(Mockito.anyString())).thenCallRealMethod(); @@ -113,12 +114,14 @@ public void canManageWebhook() throws Exception { webTestHelper.post("/webhooks.subscribe", subscribePayload) .andExpect(status().isOk()) .andExpect(jsonPath("$.id", equalTo(webhookId))) + .andExpect(jsonPath("$.name", equalTo(name))) .andExpect(jsonPath("$.url", equalTo(url))) .andExpect(jsonPath("$.headers['X-Auth']", equalTo(xAuthHeader))); retryOnException(() -> webTestHelper.post("/webhooks.update", updatePayload) .andExpect(status().isOk()) .andExpect(jsonPath("$.id", equalTo(webhookId))) + .andExpect(jsonPath("$.name", equalTo(name))) .andExpect(jsonPath("$.url", equalTo(url))) .andExpect(jsonPath("$.headers['X-Auth']", equalTo(xAuthHeader))) .andExpect(jsonPath("$.events", hasSize(2))), @@ -128,6 +131,7 @@ public void canManageWebhook() throws Exception { retryOnException(() -> webTestHelper.post("/webhooks.info", infoPayload) .andExpect(status().isOk()) .andExpect(jsonPath("$.id", equalTo(webhookId))) + .andExpect(jsonPath("$.name", equalTo(name))) .andExpect(jsonPath("$.url", equalTo(url))) .andExpect(jsonPath("$.headers['X-Auth']", equalTo(xAuthHeader))), "Webhook was not stored" @@ -145,6 +149,7 @@ public void canListWebhooks() throws Exception { new ProducerRecord<>(Topics.applicationCommunicationWebhooks.name(), UUID.randomUUID().toString(), Webhook.newBuilder() .setEndpoint("http://endpoint.com/webhook") + .setName("webhook name") .setId(UUID.randomUUID().toString()) .setStatus(Status.Subscribed) .setSubscribedAt(Instant.now().toEpochMilli()) @@ -153,6 +158,7 @@ public void canListWebhooks() throws Exception { new ProducerRecord<>(Topics.applicationCommunicationWebhooks.name(), UUID.randomUUID().toString(), Webhook.newBuilder() .setEndpoint("http://endpoint.com/webhook-2") + .setName("webhook name 2") .setId(UUID.randomUUID().toString()) .setStatus(Status.Subscribed) .setSubscribedAt(Instant.now().toEpochMilli()) @@ -161,6 +167,7 @@ public void canListWebhooks() throws Exception { new ProducerRecord<>(Topics.applicationCommunicationWebhooks.name(), UUID.randomUUID().toString(), Webhook.newBuilder() .setEndpoint("http://endpoint.com/webhook-2") + .setName("webhook name 2") .setId(UUID.randomUUID().toString()) .setStatus(Status.Unsubscribed) .setSubscribedAt(Instant.now().toEpochMilli()) @@ -170,7 +177,7 @@ public void canListWebhooks() throws Exception { retryOnException(() -> webTestHelper.post("/webhooks.list") .andExpect(status().isOk()) - .andExpect(jsonPath("$.data", hasSize(lessThanOrEqualTo(2)))), + .andExpect(jsonPath("$.data", hasSize(lessThanOrEqualTo(4)))), "list did not return all results" ); } diff --git a/backend/api/contacts/BUILD b/backend/api/contacts/BUILD index 09978d180f..2d965faccc 100644 --- a/backend/api/contacts/BUILD +++ b/backend/api/contacts/BUILD @@ -7,6 +7,7 @@ load("//tools/build:container_release.bzl", "container_release") app_deps = [ "//backend:base_app", "//backend/model/conversation", + "//backend/model/contacts", "//backend/model/metadata", "//backend/model/message", "//lib/java/uuid", diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/ContactsController.java b/backend/api/contacts/src/main/java/co/airy/core/contacts/ContactsController.java index 6fd71452e2..a9e696b302 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/ContactsController.java +++ b/backend/api/contacts/src/main/java/co/airy/core/contacts/ContactsController.java @@ -1,7 +1,7 @@ package co.airy.core.contacts; import co.airy.avro.communication.Metadata; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import co.airy.core.contacts.payload.ContactInfoRequestPayload; import co.airy.core.contacts.payload.ContactResponsePayload; import co.airy.core.contacts.payload.ContactWithMergeHistoryResponsePayload; diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/Stores.java b/backend/api/contacts/src/main/java/co/airy/core/contacts/Stores.java index 08d3f856f5..cf619b6119 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/Stores.java +++ b/backend/api/contacts/src/main/java/co/airy/core/contacts/Stores.java @@ -2,8 +2,8 @@ import co.airy.avro.communication.Message; import co.airy.avro.communication.Metadata; -import co.airy.core.contacts.dto.Contact; -import co.airy.core.contacts.dto.ConversationContact; +import co.airy.model.contacts.Contact; +import co.airy.model.contacts.ConversationContact; import co.airy.kafka.schema.application.ApplicationCommunicationContacts; import co.airy.kafka.schema.application.ApplicationCommunicationMessages; import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; @@ -35,8 +35,8 @@ import java.util.Map; import java.util.UUID; -import static co.airy.core.contacts.MetadataRepository.newContactMetadata; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.CONVERSATIONS; +import static co.airy.model.contacts.MetadataRepository.newContactMetadata; +import static co.airy.model.contacts.Contact.MetadataKeys.CONVERSATIONS; import static co.airy.model.metadata.MetadataKeys.ConversationKeys.CONTACT; import static co.airy.model.metadata.MetadataRepository.getId; import static co.airy.model.metadata.MetadataRepository.getSubject; diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactResponsePayload.java b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactResponsePayload.java index cf1731a45c..9f886b33a5 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactResponsePayload.java +++ b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactResponsePayload.java @@ -1,6 +1,6 @@ package co.airy.core.contacts.payload; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactWithMergeHistoryResponsePayload.java b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactWithMergeHistoryResponsePayload.java index d5d575195e..bf9bf2bae9 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactWithMergeHistoryResponsePayload.java +++ b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/ContactWithMergeHistoryResponsePayload.java @@ -1,6 +1,6 @@ package co.airy.core.contacts.payload; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/CreateContactPayload.java b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/CreateContactPayload.java index 86305f1c30..e6dd1c867f 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/CreateContactPayload.java +++ b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/CreateContactPayload.java @@ -1,6 +1,6 @@ package co.airy.core.contacts.payload; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/UpdateContactPayload.java b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/UpdateContactPayload.java index 7307d68cc3..9cd36caf9a 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/UpdateContactPayload.java +++ b/backend/api/contacts/src/main/java/co/airy/core/contacts/payload/UpdateContactPayload.java @@ -1,6 +1,6 @@ package co.airy.core.contacts.payload; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/backend/api/contacts/src/test/java/co/airy/core/contacts/DeleteContactTest.java b/backend/api/contacts/src/test/java/co/airy/core/contacts/DeleteContactTest.java index 71fa0db0e9..8cce5248ad 100644 --- a/backend/api/contacts/src/test/java/co/airy/core/contacts/DeleteContactTest.java +++ b/backend/api/contacts/src/test/java/co/airy/core/contacts/DeleteContactTest.java @@ -2,7 +2,7 @@ import co.airy.spring.core.AirySpringBootApplication; import co.airy.spring.test.WebTestHelper; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import co.airy.core.contacts.payload.DeleteContactPayload; import co.airy.core.contacts.util.TestContact; import co.airy.core.contacts.util.Topics; diff --git a/backend/api/contacts/src/test/java/co/airy/core/contacts/ListContactsTest.java b/backend/api/contacts/src/test/java/co/airy/core/contacts/ListContactsTest.java index ab2fc37ae9..c7fae3cf4f 100644 --- a/backend/api/contacts/src/test/java/co/airy/core/contacts/ListContactsTest.java +++ b/backend/api/contacts/src/test/java/co/airy/core/contacts/ListContactsTest.java @@ -1,6 +1,6 @@ package co.airy.core.contacts; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import co.airy.core.contacts.util.TestContact; import co.airy.core.contacts.util.Topics; import co.airy.kafka.test.KafkaTestHelper; diff --git a/backend/api/contacts/src/test/java/co/airy/core/contacts/UpdateContactsTest.java b/backend/api/contacts/src/test/java/co/airy/core/contacts/UpdateContactsTest.java index a90594c2b0..f2f5fc0d35 100644 --- a/backend/api/contacts/src/test/java/co/airy/core/contacts/UpdateContactsTest.java +++ b/backend/api/contacts/src/test/java/co/airy/core/contacts/UpdateContactsTest.java @@ -1,6 +1,6 @@ package co.airy.core.contacts; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import co.airy.core.contacts.util.TestContact; import co.airy.core.contacts.util.Topics; import co.airy.kafka.test.KafkaTestHelper; diff --git a/backend/api/contacts/src/test/java/co/airy/core/contacts/util/TestContact.java b/backend/api/contacts/src/test/java/co/airy/core/contacts/util/TestContact.java index e23b5a94de..43f67c8bfc 100644 --- a/backend/api/contacts/src/test/java/co/airy/core/contacts/util/TestContact.java +++ b/backend/api/contacts/src/test/java/co/airy/core/contacts/util/TestContact.java @@ -1,6 +1,6 @@ package co.airy.core.contacts.util; -import co.airy.core.contacts.dto.Contact; +import co.airy.model.contacts.Contact; import co.airy.spring.test.WebTestHelper; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.JsonNode; diff --git a/backend/avro/webhook.avsc b/backend/avro/webhook.avsc index 9947320ac0..4702359195 100644 --- a/backend/avro/webhook.avsc +++ b/backend/avro/webhook.avsc @@ -7,6 +7,14 @@ "name": "id", "type": "string" }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, { "name": "events", "type": { diff --git a/backend/model/contacts/BUILD b/backend/model/contacts/BUILD new file mode 100644 index 0000000000..eb9858919e --- /dev/null +++ b/backend/model/contacts/BUILD @@ -0,0 +1,17 @@ +load("@com_github_airyhq_bazel_tools//lint:buildifier.bzl", "check_pkg") +load("//tools/build:java_library.bzl", "custom_java_library") + +custom_java_library( + name = "contacts", + srcs = glob(["src/main/java/co/airy/model/contacts/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//:jackson", + "//:lombok", + "//backend/model/conversation", + "//backend/model/metadata", + "//lib/java/log", + ], +) + +check_pkg(name = "buildifier") diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/dto/Contact.java b/backend/model/contacts/src/main/java/co/airy/model/contacts/Contact.java similarity index 93% rename from backend/api/contacts/src/main/java/co/airy/core/contacts/dto/Contact.java rename to backend/model/contacts/src/main/java/co/airy/model/contacts/Contact.java index 0107ffc94b..6238c8c134 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/dto/Contact.java +++ b/backend/model/contacts/src/main/java/co/airy/model/contacts/Contact.java @@ -1,4 +1,4 @@ -package co.airy.core.contacts.dto; +package co.airy.model.contacts; import co.airy.avro.communication.Metadata; import co.airy.log.AiryLoggerFactory; @@ -27,19 +27,19 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static co.airy.core.contacts.MetadataRepository.newContactMetadata; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.ADDRESS; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.AVATAR_URL; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.CONVERSATIONS; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.CREATED_AT; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.DISPLAY_NAME; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.GENDER; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.LOCALE; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.ORGANIZATION_NAME; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.MERGE_HISTORY; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.TIMEZONE; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.TITLE; -import static co.airy.core.contacts.dto.Contact.MetadataKeys.VIA; +import static co.airy.model.contacts.MetadataRepository.newContactMetadata; +import static co.airy.model.contacts.Contact.MetadataKeys.ADDRESS; +import static co.airy.model.contacts.Contact.MetadataKeys.AVATAR_URL; +import static co.airy.model.contacts.Contact.MetadataKeys.CONVERSATIONS; +import static co.airy.model.contacts.Contact.MetadataKeys.CREATED_AT; +import static co.airy.model.contacts.Contact.MetadataKeys.DISPLAY_NAME; +import static co.airy.model.contacts.Contact.MetadataKeys.GENDER; +import static co.airy.model.contacts.Contact.MetadataKeys.LOCALE; +import static co.airy.model.contacts.Contact.MetadataKeys.ORGANIZATION_NAME; +import static co.airy.model.contacts.Contact.MetadataKeys.MERGE_HISTORY; +import static co.airy.model.contacts.Contact.MetadataKeys.TIMEZONE; +import static co.airy.model.contacts.Contact.MetadataKeys.TITLE; +import static co.airy.model.contacts.Contact.MetadataKeys.VIA; import static co.airy.model.metadata.MetadataRepository.getSubject; import static java.util.stream.Collectors.toMap; diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/dto/ConversationContact.java b/backend/model/contacts/src/main/java/co/airy/model/contacts/ConversationContact.java similarity index 91% rename from backend/api/contacts/src/main/java/co/airy/core/contacts/dto/ConversationContact.java rename to backend/model/contacts/src/main/java/co/airy/model/contacts/ConversationContact.java index 41b9f73cf2..e7ba65c989 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/dto/ConversationContact.java +++ b/backend/model/contacts/src/main/java/co/airy/model/contacts/ConversationContact.java @@ -1,4 +1,4 @@ -package co.airy.core.contacts.dto; +package co.airy.model.contacts; import co.airy.model.conversation.Conversation; import co.airy.model.metadata.dto.MetadataMap; diff --git a/backend/api/contacts/src/main/java/co/airy/core/contacts/MetadataRepository.java b/backend/model/contacts/src/main/java/co/airy/model/contacts/MetadataRepository.java similarity index 94% rename from backend/api/contacts/src/main/java/co/airy/core/contacts/MetadataRepository.java rename to backend/model/contacts/src/main/java/co/airy/model/contacts/MetadataRepository.java index d4650b1695..b467a48346 100644 --- a/backend/api/contacts/src/main/java/co/airy/core/contacts/MetadataRepository.java +++ b/backend/model/contacts/src/main/java/co/airy/model/contacts/MetadataRepository.java @@ -1,4 +1,4 @@ -package co.airy.core.contacts; +package co.airy.model.contacts; import co.airy.avro.communication.Metadata; import co.airy.model.metadata.Subject; diff --git a/backend/sources/facebook/events-router/BUILD b/backend/sources/facebook/events-router/BUILD index 3b0e648a1e..1f9638ce6b 100644 --- a/backend/sources/facebook/events-router/BUILD +++ b/backend/sources/facebook/events-router/BUILD @@ -5,6 +5,7 @@ load("//tools/build:container_release.bzl", "container_release") app_deps = [ "//backend:base_app", + "//:springboot_actuator", "//backend/model/channel", "//backend/model/message", "//backend/model/metadata", diff --git a/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java b/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java index 19d8ed56a9..526a30640e 100644 --- a/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java +++ b/backend/sources/facebook/events-router/src/main/java/co/airy/core/sources/facebook/EventsRouter.java @@ -26,6 +26,8 @@ import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; import org.slf4j.Logger; import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @@ -40,7 +42,7 @@ import static java.util.stream.Collectors.toList; @Component -public class EventsRouter implements DisposableBean, ApplicationListener { +public class EventsRouter implements HealthIndicator, DisposableBean, ApplicationListener { private static final Logger log = AiryLoggerFactory.getLogger(EventsRouter.class); private final String metadataStore = "metadata-store"; @@ -153,6 +155,15 @@ public void destroy() { } } + @Override + public Health health() { + if (streams == null || !streams.state().isRunningOrRebalancing()) { + return Health.down().build(); + } + streams.acquireLocalStore(metadataStore); + return Health.up().build(); + } + // visible for testing KafkaStreams.State getStreamState() { return streams.state(); diff --git a/backend/sources/google/events-router/BUILD b/backend/sources/google/events-router/BUILD index c99d2a2b3a..ab380ba42f 100644 --- a/backend/sources/google/events-router/BUILD +++ b/backend/sources/google/events-router/BUILD @@ -5,6 +5,7 @@ load("//tools/build:container_release.bzl", "container_release") app_deps = [ "//backend:base_app", + "//:springboot_actuator", "//backend/model/channel", "//backend/model/message", "//backend/model/metadata", diff --git a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java index 12876a31c1..42e13c1e63 100644 --- a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java +++ b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java @@ -22,6 +22,8 @@ import org.slf4j.Logger; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @@ -35,7 +37,7 @@ import static co.airy.model.metadata.MetadataRepository.newConversationMetadata; @Component -public class EventsRouter implements DisposableBean, ApplicationListener { +public class EventsRouter implements HealthIndicator, DisposableBean, ApplicationListener { private static final String appId = "sources.google.EventsRouter"; private static final Logger log = AiryLoggerFactory.getLogger(EventsRouter.class); @@ -59,6 +61,14 @@ public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { startStream(); } + @Override + public Health health() { + if (streams == null || !streams.state().isRunningOrRebalancing()) { + return Health.down().build(); + } + return Health.up().build(); + } + private void startStream() { final StreamsBuilder builder = new StreamsBuilder(); final String applicationCommunicationMessages = new ApplicationCommunicationMessages().name(); diff --git a/backend/sources/twilio/events-router/BUILD b/backend/sources/twilio/events-router/BUILD index 0e7bd4b364..b3c2ea443f 100644 --- a/backend/sources/twilio/events-router/BUILD +++ b/backend/sources/twilio/events-router/BUILD @@ -5,6 +5,7 @@ load("//tools/build:container_release.bzl", "container_release") app_deps = [ "//backend:base_app", + "//:springboot_actuator", "//backend/model/channel", "//backend/model/message", "//lib/java/uuid", diff --git a/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/EventsRouter.java b/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/EventsRouter.java index 76307245c9..c9bc8a6d4f 100644 --- a/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/EventsRouter.java +++ b/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/EventsRouter.java @@ -16,12 +16,14 @@ import org.apache.kafka.streams.kstream.KTable; import org.slf4j.Logger; import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component -public class EventsRouter implements DisposableBean, ApplicationListener { +public class EventsRouter implements HealthIndicator, DisposableBean, ApplicationListener { private static final Logger log = AiryLoggerFactory.getLogger(EventsRouter.class); public static final String appId = "sources.twilio.EventsRouter"; @@ -100,6 +102,14 @@ public void destroy() { } } + @Override + public Health health() { + if (streams == null || !streams.state().isRunningOrRebalancing()) { + return Health.down().build(); + } + return Health.up().build(); + } + /** * Visible For Testing * diff --git a/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Stores.java b/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Stores.java index 209986168e..686f40330b 100644 --- a/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Stores.java +++ b/backend/webhook/consumer/src/main/java/co/airy/core/webhook/consumer/Stores.java @@ -7,13 +7,15 @@ import org.apache.kafka.streams.StreamsBuilder; import org.apache.kafka.streams.kstream.Materialized; import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component -public class Stores implements ApplicationListener, DisposableBean { +public class Stores implements HealthIndicator, ApplicationListener, DisposableBean { private static final String appId = "webhook.Consumer"; private final String webhooksStore = "webhooks-store"; private final KafkaStreamsWrapper streams; @@ -49,6 +51,14 @@ public void onApplicationEvent(ApplicationStartedEvent event) { startStream(); } + @Override + public Health health() { + if (streams == null || !streams.state().isRunningOrRebalancing()) { + return Health.down().build(); + } + return Health.up().build(); + } + // visible for testing KafkaStreams.State getStreamState() { return streams.state(); diff --git a/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java b/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java index c5153fd761..0fdf1fc701 100644 --- a/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java +++ b/backend/webhook/consumer/src/test/java/co/airy/core/webhook/consumer/ConsumerTest.java @@ -100,6 +100,7 @@ void canSendOutEvents() throws Exception { final String endpoint = "http://customer-endpoint.com/webhook"; final Webhook webhook = Webhook.newBuilder() .setEndpoint(endpoint) + .setName("webhook name") .setHeaders(Map.of( "user-defined", "header" )) diff --git a/backend/webhook/publisher/src/test/java/co/airy/core/webhook/publisher/PublisherTest.java b/backend/webhook/publisher/src/test/java/co/airy/core/webhook/publisher/PublisherTest.java index 7045925408..79a7a054e0 100644 --- a/backend/webhook/publisher/src/test/java/co/airy/core/webhook/publisher/PublisherTest.java +++ b/backend/webhook/publisher/src/test/java/co/airy/core/webhook/publisher/PublisherTest.java @@ -96,6 +96,7 @@ void canPublishMessageToQueue() throws Exception { final Webhook acceptAll = Webhook.newBuilder() .setEndpoint("http://endpoint.com/accept") + .setName("webhook name") .setId(UUID.randomUUID().toString()) .setStatus(Status.Subscribed) .setSubscribedAt(Instant.now().toEpochMilli()) @@ -103,6 +104,7 @@ void canPublishMessageToQueue() throws Exception { final Webhook selective = Webhook.newBuilder() .setEndpoint("http://endpoint.com/selective") + .setName("webhook name") .setEvents(List.of( EventType.MESSAGE_CREATED.getEventType(), EventType.CONVERSATION_UPDATED.getEventType() diff --git a/cli/go.sum b/cli/go.sum index d50293126d..b652660005 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -94,7 +94,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= diff --git a/docs/docs/api/webhook.md b/docs/docs/api/webhook.md index cc7b61ab12..8e36e16da5 100644 --- a/docs/docs/api/webhook.md +++ b/docs/docs/api/webhook.md @@ -42,6 +42,7 @@ Subscribes the webhook for the first time or update its parameters. { "url": "https://endpoint.com/webhook", // required "id": "3e639566-29fa-450d-a59f-ae3c25d7260f", + "name": "Customer relationship tool", "events": ["message.created", "message.updated", "conversation.updated", "channel.updated"], "headers": { "X-Custom-Header": "e.g. authentication token" @@ -51,9 +52,10 @@ Subscribes the webhook for the first time or update its parameters. ``` - `url` Endpoint to be called when sending events. -- `id` (optional) provide for updates -- `headers` (optional) HTTP headers to set on each request (useful for authentication) -- `signature_key` (optional) when set, the webhook will also sent a header `X-Airy-Content-Signature` that contains the SHA256 HMAC of the specified key and the content. +- `id` (optional) provide for updates. +- `name` (optional) name to identify the webhook. +- `headers` (optional) HTTP headers to set on each request (useful for authentication). +- `signature_key` (optional) when set, the webhook will also send a header `X-Airy-Content-Signature` that contains the SHA256 HMAC of the specified key and the content. - `events` (optional) List of event types to receive. [See below](#events) for a detailed list. Omit to receive all event types. **Sample response** @@ -62,7 +64,7 @@ Subscribes the webhook for the first time or update its parameters. { "id": "3e639566-29fa-450d-a59f-ae3c25d7260f", "name": "Customer relationship tool", // optional - "url": "https://endpoint.com/webhook", + "url": "https://endpoint.com/webhook", // optional "events": [ // optional "message.created", @@ -86,8 +88,18 @@ Subscribes the webhook for the first time or update its parameters. ```json5 { - "url": "https://endpoint.com/webhook", + "id": "3e639566-29fa-450d-a59f-ae3c25d7260f", + "name": "Customer relationship tool", // optional + "url": "https://endpoint.com/webhook", // optional + "events": [ + // optional + "message.created", + "message.updated", + "conversation.updated", + "channel.updated" + ], "headers": { + // optional "X-Custom-Header": "custom-code-for-header" }, "status": "Unsubscribed" @@ -105,6 +117,7 @@ Update the webhook parameters. ```json5 { "id": "3e639566-29fa-450d-a59f-ae3c25d7260f", // required + "name": "Customer tool for relationship", // optional "url": "https://endpoint.com/webhook", // optional "events": ["message.created", "message.updated", "conversation.updated", "channel.updated"], // optional "headers": { @@ -125,7 +138,7 @@ Update the webhook parameters. ```json5 { "id": "3e639566-29fa-450d-a59f-ae3c25d7260f", - "name": "Customer relationship tool", // optional + "name": "Customer tool for relationship", // optional "url": "https://endpoint.com/webhook", "events": [ // optional @@ -140,7 +153,6 @@ Update the webhook parameters. }, "status": "Subscribed" } -} ``` ## List @@ -153,16 +165,19 @@ List of subscribed webhooks. { "data": [ { - "name": "Customer relationship tool", + "name": "Customer relationship tool", // optional "url": "https://endpoint.com/webhook", "headers": { + // optional "X-Custom-Header": "custom-code-for-header" - } + }, + "status": "Subscribed" }, { - "name": "Datalake connector", + "name": "Datalake connector", // optional "url": "https://other-endpoint.com/webhook", - "events": ["conversation.updated"] + "events": ["conversation.updated"], // optional + "status": "Unsubscribed" } ] } @@ -184,12 +199,21 @@ List of subscribed webhooks. ```json5 { - "status": "Subscribed", - "name": "Customer relationship tool", + "id": "3e639566-29fa-450d-a59f-ae3c25d7260f", + "name": "Customer tool for relationship", // optional "url": "https://endpoint.com/webhook", + "events": [ + // optional + "message.created", + "message.updated", + "conversation.updated", + "channel.updated" + ], "headers": { + // optional "X-Custom-Header": "custom-code-for-header" - } + }, + "status": "Subscribed" } ``` diff --git a/docs/docs/getting-started/installation/security.md b/docs/docs/getting-started/installation/security.md index 73dbf3e6ab..0c3bdf0222 100644 --- a/docs/docs/getting-started/installation/security.md +++ b/docs/docs/getting-started/installation/security.md @@ -24,6 +24,10 @@ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer my-to ``` +:::note +Both the `systemToken` and the `jwtSecret` need to be set in the `airy.yaml` file for the API security to work properly. If one of the values is omitted, the API will not be protected. +::: + ## Configuring OIDC Setting up the `systemToken` will secure the API, but it means that UI clients will no longer work since API keys are not meant for web authentication. This is why Airy Core also supports [Open Id Connect (OIDC)](https://openid.net/connect/) to allow your agents to authenticate via an external provider. If you configure this in addition to the system token then both types of authentication will work when requesting APIs. diff --git a/docs/docs/ui/control-center/catalog.md b/docs/docs/ui/control-center/catalog.md index 6a712b1c98..779d9d53c8 100644 --- a/docs/docs/ui/control-center/catalog.md +++ b/docs/docs/ui/control-center/catalog.md @@ -5,15 +5,19 @@ sidebar_label: Catalog import useBaseUrl from '@docusaurus/useBaseUrl'; -The Catalog page of the [Control Center](/ui/control-center/introduction) lists the available [connectors](/sources/introduction) that can be connected to an Airy Core app. You can choose which connector you want to add and configure it easily (see [Configuration](catalog#configuration) below). +The Catalog page of the [Control Center](/ui/control-center/introduction) lists both the [connectors](/sources/introduction) that have been installed and those that are not installed. + +In the Catalog 'Not Installed' list, you can choose which connector you want to add and configure it easily (see [Configuration](catalog#configuration) below). A connector is listed in the [Connectors](connectors) page once it has been successfully connected. ## Example -The screenshots below come from a sample Airy Core app. Its Control Center UI show that the app is already connected to the [Airy Live Chat](/sources/chatplugin/quickstart), [Facebook Messenger](/sources/facebook), [WhatsApp](/sources/whatsapp-twilio), [Google Business Messages](/sources/google), and [Instagram](/sources/instagram) connectors. The [SMS](/sources/sms-twilio) connector is listed in the Catalog: it hasn't been added to the app yet and is available to connect. -Control Center Connectors Example -Control Center Catalog Example +The screenshots below come from a sample Airy Core app's Control Center UI. + +The Catalog shows that the app is already connected to the [Airy Live Chat](/sources/chatplugin/quickstart), [Facebook Messenger](/sources/facebook), [WhatsApp](/sources/whatsapp-twilio), [Google Business Messages](/sources/google), and [Instagram](/sources/instagram) connectors. The [SMS](/sources/sms-twilio) connector hasn't been installed yet and is available to connect. + +Control Center Catalog Example ## Configuration diff --git a/docs/docs/ui/control-center/components.md b/docs/docs/ui/control-center/components.md deleted file mode 100644 index fc2d8659e9..0000000000 --- a/docs/docs/ui/control-center/components.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Components -sidebar_label: Components ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; - -The Components section of the [Control Center](/ui/control-center/introduction) provides a graphical overview of Airy Core's different [components](/getting-started/components) and their status. - -The data displayed in this comprehensible table is served by the [client.config endpoint](/api/endpoints/client-config). - -Control Center diff --git a/docs/docs/ui/control-center/introduction.md b/docs/docs/ui/control-center/introduction.md index 231bac874b..b13877cd0d 100644 --- a/docs/docs/ui/control-center/introduction.md +++ b/docs/docs/ui/control-center/introduction.md @@ -8,23 +8,26 @@ import ButtonBox from "@site/src/components/ButtonBox"; import ComponentsSVG from "@site/static/icons/componentsIcon.svg"; import ConnectorsSVG from "@site/static/icons/connectorsIcon.svg"; import CatalogSVG from "@site/static/icons/catalogIcon.svg"; +import WebhooksSVG from "@site/static/icons/webhooksIcon.svg"; import useBaseUrl from '@docusaurus/useBaseUrl'; -The Control Center serves as a technical dashboard: it provides a graphical overview of an Airy Core instance's [components](/getting-started/components) and [connectors](connectors) while its [catalog](catalog) enables to choose and configure additional [connectors](connectors). +The Control Center serves as the technical dashboard of your Airy Core app. + +It provides both a graphical overview and a way to manage your app's [components](/getting-started/components), [connectors](connectors), and [webhooks](/api/webhook). Its [catalog](catalog) enables you to choose and configure additional [connectors](connectors). } iconInvertible={true} - title='Components' - description="Get an overview of your app's components and their status" - link='ui/control-center/components' + title='Status' + description="Get an overview and manage your app's components and their status" + link='ui/control-center/status' /> } title='Connectors' iconInvertible={true} - description="View and manage your app's connectors" + description="View and configure your app's connectors" link='ui/control-center/connectors' /> + } + title='Webhooks' + iconInvertible={true} + description="Manage, update and connect webhooks" + link='ui/control-center/webhooks' + />
-Screenshots of the Control Center UI: -Control Center Components Demo -Control Center Connectors Demo -Control Center Catalog Demo +Screenshots of the Control Center's Status, Connectors, Catalog and Webhooks pages. +Control Center Status Demo +Control Center Connectors Demo +Control Center Catalog Demo +Control Center Webhooks Demo diff --git a/docs/docs/ui/control-center/status.md b/docs/docs/ui/control-center/status.md new file mode 100644 index 0000000000..24294e2595 --- /dev/null +++ b/docs/docs/ui/control-center/status.md @@ -0,0 +1,12 @@ +--- +title: Status +sidebar_label: Status +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +The Status section of the [Control Center](/ui/control-center/introduction) provides a high-level overview of all your Airy Core app's [components](/getting-started/components) and their status. It also allows you to enable [components](/getting-started/components) through a toggle and see their status change in real time. + +The data displayed in this comprehensible table is served by the [client.config endpoint](/api/endpoints/client-config). + +Control Center diff --git a/docs/docs/ui/control-center/webhooks.md b/docs/docs/ui/control-center/webhooks.md new file mode 100644 index 0000000000..60721bd239 --- /dev/null +++ b/docs/docs/ui/control-center/webhooks.md @@ -0,0 +1,16 @@ +--- +title: Webhooks +sidebar_label: Webhooks +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +The Webhooks page of the [Control Center](/ui/control-center/introduction) lists all subscribed and unsubscribed [webhooks](/api/webhook) that have been connected in the past. + +## Example + +The screenshots below come from a sample Airy Core app's Control Center UI. + +On this page you are able to check your connected [webhooks](/api/webhook), update already connected [webhooks](/api/webhook) or connect new [webhooks](/api/webhook). + +Control Center Webhooks Example diff --git a/docs/docs/ui/overview.md b/docs/docs/ui/overview.md index 440b66d8f8..5afc84d6ba 100644 --- a/docs/docs/ui/overview.md +++ b/docs/docs/ui/overview.md @@ -14,9 +14,9 @@ import TLDR from "@site/src/components/TLDR"; Not every message can be handled by code, which is why Airy comes with different UIs ready for you and your team to use. -While the [Chat Plugin](sources/chatplugin/overview.md) is the open-source chat UI for your website and app visitors, Airy UI offers all of the UI interfaces you need internally for a messaging platform. +While the [Chat Plugin](sources/chatplugin/overview.md) is the open-source chat UI for your website and app visitors, Airy UI offers all of the interfaces you need internally for a messaging platform. -Airy UI comes with two open-source, customizable separate UIs: the [Inbox](inbox/introduction) and the [Control Center](control-center/introduction). +Airy UI comes with two open-source, customizable separate UIs: the [Inbox](inbox/introduction) and the [Control Center](control-center/introduction). Both can be accessed through a common landing page (see screenshot below). The [Inbox](inbox/introduction) offers [instant messaging](inbox/messenger) along with [search, filtering](inbox/messenger#search-and-filter) and [tags](inbox/tags) to organize your conversations. @@ -39,6 +39,13 @@ The [Control Center](control-center/introduction) provides a technical dashboard /> +Screenshot of the Airy UI landing page: +UI Demo + +
+
+
+ Screenshot of the [Inbox](inbox/introduction): Inbox Messenger Demo @@ -47,4 +54,4 @@ Screenshot of the [Inbox](inbox/introduction):
Screenshot of the [Control Center](control-center/introduction): -Control Center Demo +Control Center Demo diff --git a/docs/sidebars.js b/docs/sidebars.js index 717ad75a1b..2dc32ed79b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -85,9 +85,10 @@ module.exports = { { 'Control Center': [ 'ui/control-center/introduction', - 'ui/control-center/components', + 'ui/control-center/status', 'ui/control-center/connectors', 'ui/control-center/catalog', + 'ui/control-center/webhooks', ], }, ], diff --git a/docs/static/icons/webhooksIcon.svg b/docs/static/icons/webhooksIcon.svg new file mode 100644 index 0000000000..5f4013ca0e --- /dev/null +++ b/docs/static/icons/webhooksIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/static/img/sources/google/googleConnect.png b/docs/static/img/sources/google/googleConnect.png index 0ade0b7055..344b577b7b 100644 Binary files a/docs/static/img/sources/google/googleConnect.png and b/docs/static/img/sources/google/googleConnect.png differ diff --git a/docs/static/img/sources/instagram/instagramConnect.png b/docs/static/img/sources/instagram/instagramConnect.png index d10c204ac2..ee01687ebf 100644 Binary files a/docs/static/img/sources/instagram/instagramConnect.png and b/docs/static/img/sources/instagram/instagramConnect.png differ diff --git a/docs/static/img/sources/twilio/whatsAppConnect.png b/docs/static/img/sources/twilio/whatsAppConnect.png index f0f148b4e0..e70f65552f 100644 Binary files a/docs/static/img/sources/twilio/whatsAppConnect.png and b/docs/static/img/sources/twilio/whatsAppConnect.png differ diff --git a/docs/static/img/ui/controlCenterCatalog.png b/docs/static/img/ui/controlCenterCatalog.png new file mode 100644 index 0000000000..b1e64a19b4 Binary files /dev/null and b/docs/static/img/ui/controlCenterCatalog.png differ diff --git a/docs/static/img/ui/controlCenterCatalogDemo.png b/docs/static/img/ui/controlCenterCatalogDemo.png deleted file mode 100644 index b8eb572fc5..0000000000 Binary files a/docs/static/img/ui/controlCenterCatalogDemo.png and /dev/null differ diff --git a/docs/static/img/ui/controlCenterCatalogExample.png b/docs/static/img/ui/controlCenterCatalogExample.png deleted file mode 100644 index 468ebfb34c..0000000000 Binary files a/docs/static/img/ui/controlCenterCatalogExample.png and /dev/null differ diff --git a/docs/static/img/ui/controlCenterComponents.png b/docs/static/img/ui/controlCenterComponents.png deleted file mode 100644 index 5e4e87fd05..0000000000 Binary files a/docs/static/img/ui/controlCenterComponents.png and /dev/null differ diff --git a/docs/static/img/ui/controlCenterConnectors.png b/docs/static/img/ui/controlCenterConnectors.png index 5d1e45d103..379dd09271 100644 Binary files a/docs/static/img/ui/controlCenterConnectors.png and b/docs/static/img/ui/controlCenterConnectors.png differ diff --git a/docs/static/img/ui/controlCenterConnectorsDemo.png b/docs/static/img/ui/controlCenterConnectorsDemo.png deleted file mode 100644 index 7ec8c735b8..0000000000 Binary files a/docs/static/img/ui/controlCenterConnectorsDemo.png and /dev/null differ diff --git a/docs/static/img/ui/controlCenterStatus.png b/docs/static/img/ui/controlCenterStatus.png new file mode 100644 index 0000000000..14a4bf423a Binary files /dev/null and b/docs/static/img/ui/controlCenterStatus.png differ diff --git a/docs/static/img/ui/controlCenterWebhooks.png b/docs/static/img/ui/controlCenterWebhooks.png new file mode 100644 index 0000000000..45a1758ec1 Binary files /dev/null and b/docs/static/img/ui/controlCenterWebhooks.png differ diff --git a/docs/static/img/ui/ui.png b/docs/static/img/ui/ui.png new file mode 100644 index 0000000000..e8212defc3 Binary files /dev/null and b/docs/static/img/ui/ui.png differ diff --git a/docs/yarn.lock b/docs/yarn.lock index de7c148633..0d34d08b74 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3298,11 +3298,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: sha.js "^2.4.8" cross-fetch@^3.0.4: - version "3.0.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" - integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== dependencies: - node-fetch "2.6.1" + node-fetch "2.6.7" cross-spawn@7.0.3, cross-spawn@^7.0.3: version "7.0.3" @@ -6454,10 +6454,12 @@ node-emoji@^1.10.0: dependencies: lodash.toarray "^4.4.0" -node-fetch@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" node-forge@^0.10.0: version "0.10.0" @@ -9369,6 +9371,11 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + trim-trailing-lines@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" @@ -9864,6 +9871,11 @@ web-namespaces@^1.0.0, web-namespaces@^1.1.2: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webpack-bundle-analyzer@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7" @@ -10009,6 +10021,14 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" diff --git a/frontend/chat-plugin/lib/BUILD b/frontend/chat-plugin/lib/BUILD index 31a2a36b52..d65f4d94cb 100644 --- a/frontend/chat-plugin/lib/BUILD +++ b/frontend/chat-plugin/lib/BUILD @@ -9,6 +9,14 @@ package(default_visibility = ["//visibility:public"]) ts_web_library( name = "chat-plugin", + srcs = glob( + [ + "**/*.json", + "**/*.ts", + "**/*.tsx", + ], + exclude = ["publish_package.json"], + ), tsconfig = "//frontend/chat-plugin:widget_tsconfig", deps = ts_deps + npm_deps, ) @@ -48,7 +56,7 @@ web_library( genrule( name = "npm_library", srcs = [ - "package.json", + "publish_package.json", "README.md", ":dist", ":chat-plugin", @@ -57,7 +65,8 @@ genrule( outs = ["chat-plugin_npm_lib"], cmd = """ mkdir -p $(OUTS)/{src,dist} && cp -R $(location :dist) $(OUTS) \ - && cp $(location :package.json) $(location :README.md) $(OUTS) \ + && cp $(location :README.md) $(OUTS) \ + && cp $(location :publish_package.json) $(OUTS)/package.json \ && mv $(RULEDIR)/src $(OUTS) && mv $(RULEDIR)/index.d.ts $(OUTS) """, ) diff --git a/frontend/chat-plugin/lib/package.json b/frontend/chat-plugin/lib/publish_package.json similarity index 100% rename from frontend/chat-plugin/lib/package.json rename to frontend/chat-plugin/lib/publish_package.json diff --git a/frontend/control-center/src/App.tsx b/frontend/control-center/src/App.tsx index 6502431a8a..33832a9b00 100644 --- a/frontend/control-center/src/App.tsx +++ b/frontend/control-center/src/App.tsx @@ -5,20 +5,24 @@ import Sidebar from './components/Sidebar'; import styles from './App.module.scss'; import {getClientConfig} from './actions/config'; import {Navigate, Route, Routes} from 'react-router-dom'; -import {CATALOG_ROUTE, CHANNELS_ROUTE, ROOT_ROUTE, STATUS_ROUTE, WEBHOOKS_ROUTE} from './routes/routes'; -import FacebookConnect from './pages/Channels/Providers/Facebook/Messenger/FacebookConnect'; -import ChatPluginConnect from './pages/Channels/Providers/Airy/ChatPlugin/ChatPluginConnect'; -import ConnectedChannelsList from './pages/Channels/ConnectedChannelsList'; -import TwilioSmsConnect from './pages/Channels/Providers/Twilio/SMS/TwilioSmsConnect'; -import TwilioWhatsappConnect from './pages/Channels/Providers/Twilio/WhatsApp/TwilioWhatsappConnect'; -import GoogleConnect from './pages/Channels/Providers/Google/GoogleConnect'; -import InstagramConnect from './pages/Channels/Providers/Instagram/InstagramConnect'; +import {INBOX_ROUTE, CATALOG_ROUTE, CONNECTORS_ROUTE, ROOT_ROUTE, STATUS_ROUTE, WEBHOOKS_ROUTE} from './routes/routes'; +import FacebookConnect from './pages/Connectors/Providers/Facebook/Messenger/FacebookConnect'; +import ChatPluginConnect from './pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect'; +import ConnectedChannelsList from './pages/Connectors/ConnectedChannelsList'; +import TwilioSmsConnect from './pages/Connectors/Providers/Twilio/SMS/TwilioSmsConnect'; +import TwilioWhatsappConnect from './pages/Connectors/Providers/Twilio/WhatsApp/TwilioWhatsappConnect'; +import GoogleConnect from './pages/Connectors/Providers/Google/GoogleConnect'; +import InstagramConnect from './pages/Connectors/Providers/Instagram/InstagramConnect'; import NotFound from './pages/NotFound'; -import ChannelsOutlet from './pages/Channels/ChannelsOutlet'; +import ConnectorsOutlet from './pages/Connectors/ConnectorsOutlet'; import Catalog from './pages/Catalog'; -import Channels from './pages/Channels'; +import CatalogOutlet from './pages/Catalog/CatalogOutlet'; +import Connectors from './pages/Connectors'; import Webhooks from './pages/Webhooks'; import Status from './pages/Status'; +import Inbox from './pages/Inbox'; +import ChannelsList from './pages/Inbox/ChannelsList'; +import InboxOutlet from './pages/Inbox/InboxOutlet'; const mapDispatchToProps = { getClientConfig, @@ -29,6 +33,9 @@ const connector = connect(null, mapDispatchToProps); const App = (props: ConnectedProps) => { useEffect(() => { props.getClientConfig(); + if (localStorage.getItem('theme') === 'dark') { + document.documentElement.setAttribute('data-theme', 'dark'); + } }, []); return ( @@ -37,8 +44,20 @@ const App = (props: ConnectedProps) => { - } /> - }> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + }> } /> } /> } /> @@ -46,12 +65,23 @@ const App = (props: ConnectedProps) => { } /> } /> } /> - } /> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> - } /> } /> - }> + } /> diff --git a/frontend/control-center/src/actions/channel/index.ts b/frontend/control-center/src/actions/channel/index.ts index 7fbcbbac66..bb661e20a4 100644 --- a/frontend/control-center/src/actions/channel/index.ts +++ b/frontend/control-center/src/actions/channel/index.ts @@ -17,16 +17,16 @@ import { import {HttpClientInstance} from '../../httpClient'; -const SET_CURRENT_CHANNELS = '@@channel/SET_CHANNELS'; -const ADD_CHANNELS = '@@channel/ADD_CHANNELS'; +const SET_CURRENT_CONNECTORS = '@@channel/SET_CONNECTORS'; +const ADD_CONNECTORS = '@@channel/ADD_CONNECTORS'; const SET_CHANNEL = '@@channel/SET_CHANNEL'; const DELETE_CHANNEL = '@@channel/DELETE_CHANNEL'; -export const setCurrentChannelsAction = createAction(SET_CURRENT_CHANNELS, (channels: Channel[]) => channels)< +export const setCurrentChannelsAction = createAction(SET_CURRENT_CONNECTORS, (channels: Channel[]) => channels)< Channel[] >(); -export const addChannelsAction = createAction(ADD_CHANNELS, (channels: Channel[]) => channels)(); +export const addChannelsAction = createAction(ADD_CONNECTORS, (channels: Channel[]) => channels)(); export const setChannelAction = createAction(SET_CHANNEL, (channel: Channel) => channel)(); export const deleteChannelAction = createAction(DELETE_CHANNEL, (channelId: string) => channelId)(); diff --git a/frontend/control-center/src/actions/index.ts b/frontend/control-center/src/actions/index.ts index 74bcbad990..1535d11308 100644 --- a/frontend/control-center/src/actions/index.ts +++ b/frontend/control-center/src/actions/index.ts @@ -1,2 +1,4 @@ export * from './channel'; export * from './metadata'; +export * from './config'; +export * from './webhook'; diff --git a/frontend/control-center/src/actions/webhook/index.ts b/frontend/control-center/src/actions/webhook/index.ts new file mode 100644 index 0000000000..cb6a959a73 --- /dev/null +++ b/frontend/control-center/src/actions/webhook/index.ts @@ -0,0 +1,48 @@ +import { + SubscribeWebhookRequestPayload, + UnsubscribeWebhookRequestPayload, + UpdateWebhookRequestPayload, +} from 'httpclient/src'; +import {Webhook} from 'model/Webhook'; +import _, {Dispatch} from 'redux'; +import _typesafe, {createAction} from 'typesafe-actions'; + +import {HttpClientInstance} from '../../httpClient'; + +const ADD_WEBHOOKS_TO_STORE = 'ADD_WEBHOOKS_TO_STORE'; +const SUBSCRIBE_WEBHOOK = 'SUBSCRIBE_WEBHOOK'; +const UNSUBSCRIBE_WEBHOOK = 'UNSUBSCRIBE_WEBHOOK'; +const UPDATE_WEBHOOK = 'UPDATE_WEBHOOK'; + +export const saveWebhooks = createAction(ADD_WEBHOOKS_TO_STORE, (webhook: Webhook[]) => webhook)(); +export const enableWebhook = createAction(SUBSCRIBE_WEBHOOK, (webhook: Webhook) => webhook)(); +export const disableWebhook = createAction(UNSUBSCRIBE_WEBHOOK, (webhook: Webhook) => webhook)(); +export const changeWebhook = createAction(UPDATE_WEBHOOK, (webhook: Webhook) => webhook)(); + +export const listWebhooks = () => async (dispatch: Dispatch) => { + return HttpClientInstance.listWebhooks().then((response: Webhook[]) => { + dispatch(saveWebhooks(response)); + return Promise.resolve(true); + }); +}; + +export const subscribeWebhook = (request: SubscribeWebhookRequestPayload) => async (dispatch: Dispatch) => { + return HttpClientInstance.subscribeWebhook(request).then((response: Webhook) => { + dispatch(enableWebhook(response)); + return Promise.resolve(true); + }); +}; + +export const unsubscribeWebhook = (request: UnsubscribeWebhookRequestPayload) => async (dispatch: Dispatch) => { + return HttpClientInstance.unsubscribeWebhook(request).then((response: Webhook) => { + dispatch(disableWebhook(response)); + return Promise.resolve(true); + }); +}; + +export const updateWebhook = (request: UpdateWebhookRequestPayload) => async (dispatch: Dispatch) => { + return HttpClientInstance.updateWebhook(request).then((response: Webhook) => { + dispatch(changeWebhook(response)); + return Promise.resolve(true); + }); +}; diff --git a/frontend/control-center/src/components/ChannelAvatar/index.module.scss b/frontend/control-center/src/components/ChannelAvatar/index.module.scss index 4874e4e650..847f1e043b 100644 --- a/frontend/control-center/src/components/ChannelAvatar/index.module.scss +++ b/frontend/control-center/src/components/ChannelAvatar/index.module.scss @@ -1,3 +1,5 @@ +@import 'assets/scss/colors.scss'; + .image { display: flex; align-self: center; @@ -5,6 +7,8 @@ svg { height: 100%; width: 100%; + + fill: var(--color-text-contrast); } img { diff --git a/frontend/control-center/src/components/Pagination/index.module.scss b/frontend/control-center/src/components/Pagination/index.module.scss new file mode 100644 index 0000000000..171080f750 --- /dev/null +++ b/frontend/control-center/src/components/Pagination/index.module.scss @@ -0,0 +1,24 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.buttons { + button { + border: none; + background: none; + outline: none; + color: var(--color-text-gray); + &:hover { + cursor: pointer; + color: var(--color-airy-blue); + } + } + + button:first-of-type { + margin-right: 20px; + } +} + +.pages { + @include font-base; + font-weight: 700; +} diff --git a/frontend/control-center/src/components/Pagination/index.tsx b/frontend/control-center/src/components/Pagination/index.tsx new file mode 100644 index 0000000000..75cd0e0bbc --- /dev/null +++ b/frontend/control-center/src/components/Pagination/index.tsx @@ -0,0 +1,72 @@ +import React, {useEffect, useState} from 'react'; +import styles from './index.module.scss'; + +import {ReactComponent as ChevronLeft} from 'assets/images/icons/chevronLeft.svg'; +import {ReactComponent as ChevronRight} from 'assets/images/icons/chevronRight.svg'; + +type PaginationType = { + totalCount: number; + pageCount: number; + currentPage?: number; + onPageChange: (page: number) => void; + onSearch: boolean; +}; + +export const Pagination = (props: PaginationType) => { + const {totalCount, pageCount, currentPage, onPageChange, onSearch} = props; + const [displayedItems, setDisplayedItems] = useState([1, pageCount]); + const [endReached, setEndReached] = useState(false); + const pageSize = 8; + + useEffect(() => { + currentPage * pageCount + pageCount > totalCount ? setEndReached(true) : setEndReached(false); + pageCount < pageSize && !onSearch && setDisplayedItems([1, pageCount]); + }, [currentPage, pageCount, onSearch]); + + const onNext = () => { + onPageChange(currentPage + 1); + endReached + ? setDisplayedItems([1 + pageCount * currentPage, totalCount]) + : setDisplayedItems([1 + pageCount * currentPage, pageCount + pageCount * currentPage]); + }; + + const onPrevious = () => { + onPageChange(currentPage - 1); + setDisplayedItems([1 + pageCount * currentPage - pageCount * 2, pageCount * currentPage - pageCount]); + }; + + return ( +
+
+
+ + {onSearch + ? `${totalCount} ` + : pageCount === 0 + ? `${pageCount} ` + : `${displayedItems[0]} - ${displayedItems[1]} `} + + of {totalCount} +
+ {totalCount > pageCount && ( +
+ + +
+ )} +
+
+ ); +}; diff --git a/frontend/control-center/src/components/Sidebar/index.module.scss b/frontend/control-center/src/components/Sidebar/index.module.scss index c8bc850712..47d8b8c1c4 100644 --- a/frontend/control-center/src/components/Sidebar/index.module.scss +++ b/frontend/control-center/src/components/Sidebar/index.module.scss @@ -11,7 +11,8 @@ margin-top: 88px; width: 175px; height: 100%; - background-color: white; + background-color: var(--color-background-white); + color: var(--color-text-contrast); position: fixed; } diff --git a/frontend/control-center/src/components/Sidebar/index.tsx b/frontend/control-center/src/components/Sidebar/index.tsx index 444fc63c7f..b394fe6b5b 100644 --- a/frontend/control-center/src/components/Sidebar/index.tsx +++ b/frontend/control-center/src/components/Sidebar/index.tsx @@ -2,11 +2,12 @@ import React from 'react'; import {Link} from 'react-router-dom'; import {useMatch} from 'react-router'; -import {CATALOG_ROUTE, CHANNELS_ROUTE, STATUS_ROUTE, WEBHOOKS_ROUTE} from '../../routes/routes'; +import {CATALOG_ROUTE, CONNECTORS_ROUTE, STATUS_ROUTE, WEBHOOKS_ROUTE} from '../../routes/routes'; import {ReactComponent as ConnectorsIcon} from 'assets/images/icons/gitMerge.svg'; import {ReactComponent as CatalogIcon} from 'assets/images/icons/catalogIcon.svg'; import {ReactComponent as WebhooksIcon} from 'assets/images/icons/webhooksIcon.svg'; import {ReactComponent as StatusIcon} from 'assets/images/icons/statusIcon.svg'; +// import {ReactComponent as InboxIcon} from 'assets/images/icons/inboxIcon.svg'; import styles from './index.module.scss'; import {StateModel} from '../../reducers'; @@ -34,8 +35,8 @@ const Sidebar = (props: SideBarProps) => { Status -
- +
+ Connectors @@ -52,6 +53,13 @@ const Sidebar = (props: SideBarProps) => { Webhooks
+ {/*
+
+ + + Inbox + +
*/}
Version {props.version} diff --git a/frontend/control-center/src/components/SourceInfo/index.tsx b/frontend/control-center/src/components/SourceInfo/index.tsx new file mode 100644 index 0000000000..c36e9a7d7f --- /dev/null +++ b/frontend/control-center/src/components/SourceInfo/index.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import {Source} from 'model'; +import {ReactComponent as AiryAvatarIcon} from 'assets/images/icons/airyLogo.svg'; +import {ReactComponent as MessengerAvatarIcon} from 'assets/images/icons/facebookMessengerLogoBlue.svg'; +import {ReactComponent as SMSAvatarIcon} from 'assets/images/icons/phoneIcon.svg'; +import {ReactComponent as WhatsAppAvatarIcon} from 'assets/images/icons/whatsappLogoFilled.svg'; +import {ReactComponent as GoogleAvatarIcon} from 'assets/images/icons/googleLogo.svg'; +import {ReactComponent as InstagramIcon} from 'assets/images/icons/instagramLogoFilled.svg'; +import { + cyChannelsChatPluginAddButton, + cyChannelsChatPluginList, + cyChannelsFacebookAddButton, + cyChannelsFacebookList, + cyChannelsGoogleAddButton, + cyChannelsGoogleList, + cyChannelsTwilioSmsAddButton, + cyChannelsTwilioSmsList, + cyChannelsTwilioWhatsappAddButton, + cyChannelsTwilioWhatsappList, + cyChannelsInstagramAddButton, + cyChannelsInstagramList, +} from 'handles'; +import { + CATALOG_FACEBOOK_ROUTE, + CATALOG_TWILIO_SMS_ROUTE, + CATALOG_TWILIO_WHATSAPP_ROUTE, + CONNECTORS_CONNECTED_ROUTE, + CATALOG_CHAT_PLUGIN_ROUTE, + CATALOG_GOOGLE_ROUTE, + CATALOG_INSTAGRAM_ROUTE, + CATALOG_CONNECTED_ROUTE, + CONNECTORS_FACEBOOK_ROUTE, + CONNECTORS_TWILIO_SMS_ROUTE, + CONNECTORS_TWILIO_WHATSAPP_ROUTE, + CONNECTORS_CHAT_PLUGIN_ROUTE, + CONNECTORS_GOOGLE_ROUTE, + CONNECTORS_INSTAGRAM_ROUTE, +} from '../../routes/routes'; + +export type SourceInfo = { + type: Source; + channel: boolean; + title: string; + description: string; + image: JSX.Element; + newChannelRoute: string; + channelsListRoute: string; + configKey: string; + itemInfoString: string; + dataCyAddChannelButton: string; + dataCyChannelList: string; +}; + +export const getSourcesInfo = (page: string): SourceInfo[] => { + const connectorsPage = page === 'Connectors'; + + return [ + { + type: Source.chatPlugin, + channel: true, + title: 'Airy Live Chat', + description: 'Best of class browser messenger', + image: , + newChannelRoute: connectorsPage ? CONNECTORS_CHAT_PLUGIN_ROUTE + '/new' : CATALOG_CHAT_PLUGIN_ROUTE + '/new', + channelsListRoute: connectorsPage + ? CONNECTORS_CONNECTED_ROUTE + '/chatplugin' + : CATALOG_CONNECTED_ROUTE + '/chatplugin', + configKey: 'sources-chat-plugin', + itemInfoString: 'channels', + dataCyAddChannelButton: cyChannelsChatPluginAddButton, + dataCyChannelList: cyChannelsChatPluginList, + }, + { + type: Source.facebook, + channel: true, + title: 'Messenger', + description: 'Connect multiple Facebook pages', + image: , + newChannelRoute: connectorsPage ? CONNECTORS_FACEBOOK_ROUTE + '/new' : CATALOG_FACEBOOK_ROUTE + '/new', + channelsListRoute: connectorsPage + ? CONNECTORS_CONNECTED_ROUTE + '/facebook' + : CATALOG_CONNECTED_ROUTE + '/facebook', + configKey: 'sources-facebook', + itemInfoString: 'channels', + dataCyAddChannelButton: cyChannelsFacebookAddButton, + dataCyChannelList: cyChannelsFacebookList, + }, + { + type: Source.twilioSMS, + channel: true, + title: 'SMS', + description: 'Deliver SMS with ease', + image: , + newChannelRoute: connectorsPage ? CONNECTORS_TWILIO_SMS_ROUTE + '/new' : CATALOG_TWILIO_SMS_ROUTE + '/new', + channelsListRoute: connectorsPage + ? CONNECTORS_CONNECTED_ROUTE + '/twilio.sms/#' + : CATALOG_CONNECTED_ROUTE + '/twilio.sms/#', + configKey: 'sources-twilio', + itemInfoString: 'phones', + dataCyAddChannelButton: cyChannelsTwilioSmsAddButton, + dataCyChannelList: cyChannelsTwilioSmsList, + }, + { + type: Source.twilioWhatsApp, + channel: true, + title: 'WhatsApp', + description: 'World #1 chat app', + image: , + newChannelRoute: connectorsPage + ? CONNECTORS_TWILIO_WHATSAPP_ROUTE + '/new' + : CATALOG_TWILIO_WHATSAPP_ROUTE + '/new', + channelsListRoute: connectorsPage + ? CONNECTORS_CONNECTED_ROUTE + '/twilio.whatsapp/#' + : CATALOG_CONNECTED_ROUTE + '/twilio.whatsapp/#', + configKey: 'sources-twilio', + itemInfoString: 'phones', + dataCyAddChannelButton: cyChannelsTwilioWhatsappAddButton, + dataCyChannelList: cyChannelsTwilioWhatsappList, + }, + { + type: Source.google, + channel: true, + title: 'Google Business Messages', + description: 'Be there when people search', + image: , + newChannelRoute: connectorsPage ? CONNECTORS_GOOGLE_ROUTE + '/new' : CATALOG_GOOGLE_ROUTE + '/new', + channelsListRoute: connectorsPage ? CONNECTORS_CONNECTED_ROUTE + '/google' : CATALOG_CONNECTED_ROUTE + '/google', + configKey: 'sources-google', + itemInfoString: 'channels', + dataCyAddChannelButton: cyChannelsGoogleAddButton, + dataCyChannelList: cyChannelsGoogleList, + }, + { + type: Source.instagram, + channel: true, + title: 'Instagram', + description: 'Connect multiple Instagram pages', + image: , + newChannelRoute: connectorsPage ? CONNECTORS_INSTAGRAM_ROUTE + '/new' : CATALOG_INSTAGRAM_ROUTE + '/new', + channelsListRoute: connectorsPage + ? CONNECTORS_CONNECTED_ROUTE + '/instagram' + : CATALOG_CONNECTED_ROUTE + '/instagram', + configKey: 'sources-facebook', + itemInfoString: 'channels', + dataCyAddChannelButton: cyChannelsInstagramAddButton, + dataCyChannelList: cyChannelsInstagramList, + }, + ]; +}; diff --git a/frontend/control-center/src/components/Switch/index.module.scss b/frontend/control-center/src/components/Switch/index.module.scss new file mode 100644 index 0000000000..68ca687558 --- /dev/null +++ b/frontend/control-center/src/components/Switch/index.module.scss @@ -0,0 +1,38 @@ +.switchCheckbox { + display: none; +} + +.switchLabel { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + width: 32px; + height: 14px; + background: var(--color-soft-green); + border-radius: 32px; + position: relative; + transition: background-color 0.2s; +} + +.switchLabel .switchButton { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 10px; + height: 10px; + border-radius: 45px; + transition: 0.2s; + background: #fff; + box-shadow: 0 0 2px 0 rgba(10, 10, 10, 0.29); +} + +.switchCheckbox:checked + .switchLabel .switchButton { + left: calc(100% - 2px); + transform: translateX(-100%); +} + +.switchLabel:active .switchButton { + width: 6px; +} diff --git a/frontend/control-center/src/components/Switch/index.tsx b/frontend/control-center/src/components/Switch/index.tsx new file mode 100644 index 0000000000..45d5374635 --- /dev/null +++ b/frontend/control-center/src/components/Switch/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styles from './index.module.scss'; + +type SwitchProps = { + isActive: boolean; + toggleActive: () => void; + onColor: string; + id: string; +}; + +export const Switch = (props: SwitchProps) => { + const {isActive, toggleActive, onColor, id} = props; + return ( + <> + + + + ); +}; diff --git a/frontend/control-center/src/components/TopBar/index.module.scss b/frontend/control-center/src/components/TopBar/index.module.scss index c8786d4f65..4cf2a5b046 100644 --- a/frontend/control-center/src/components/TopBar/index.module.scss +++ b/frontend/control-center/src/components/TopBar/index.module.scss @@ -1,6 +1,7 @@ @import 'assets/scss/colors.scss'; @import 'assets/scss/fonts.scss'; @import 'assets/scss/z-index.scss'; +@import 'assets/scss/animations.scss'; .topBar { display: flex; @@ -8,12 +9,13 @@ justify-content: space-between; z-index: $navigation; height: 72px; - background-color: white; - box-shadow: 0 3px 8px 0 var(--color-light-gray); + background-color: var(--color-background-white); + box-shadow: 0 3px 8px 0 var(--color-shadow-gray); position: fixed; overflow: visible; top: 0; width: 100%; + color: var(--color-text-contrast); } .airyLogo { @@ -63,13 +65,13 @@ } .dropHintOpen { - transition: 500ms ease; + transition: 300ms ease; margin-bottom: 0; transform: rotate(180deg); } .dropHintClose { - transition: 500ms ease; + transition: 300ms ease; margin-bottom: 0; transform: rotate(0deg); } @@ -96,7 +98,7 @@ .dropdownContainer { position: absolute; - background-color: white; + background-color: var(--color-background-white); border: 1px solid var(--color-light-gray); border-radius: 8px; top: 68px; @@ -241,3 +243,17 @@ box-shadow: 0px 0px 0px 3px var(--color-background-blue); } } + +.theme { + background: transparent; + border: none; + margin-left: 8px; +} + +.animateIn { + animation: topbarDropdownIn 300ms ease; +} + +.animateOut { + animation: topbarDropdownOut 300ms ease; +} diff --git a/frontend/control-center/src/components/TopBar/index.tsx b/frontend/control-center/src/components/TopBar/index.tsx index 314c34c14d..effb1e821e 100644 --- a/frontend/control-center/src/components/TopBar/index.tsx +++ b/frontend/control-center/src/components/TopBar/index.tsx @@ -2,12 +2,14 @@ import React, {useState, useCallback} from 'react'; import _, {connect, ConnectedProps} from 'react-redux'; import {ListenOutsideClick} from 'components'; import {StateModel} from '../../reducers'; +import {Toggle} from 'components'; import {ReactComponent as ShortcutIcon} from 'assets/images/icons/shortcut.svg'; import {ReactComponent as LogoutIcon} from 'assets/images/icons/signOut.svg'; import {ReactComponent as AiryLogo} from 'assets/images/logo/airyLogo.svg'; import {ReactComponent as ChevronDownIcon} from 'assets/images/icons/chevronDown.svg'; import styles from './index.module.scss'; import {env} from '../../env'; +import {useAnimation} from 'render'; interface TopBarProps { isAdmin: boolean; @@ -26,22 +28,30 @@ const connector = connect(mapStateToProps); const TopBar = (props: TopBarProps & ConnectedProps) => { const [isAccountDropdownOn, setAccountDropdownOn] = useState(false); const [isFaqDropdownOn, setFaqDropdownOn] = useState(false); + const [darkTheme, setDarkTheme] = useState(localStorage.getItem('theme') === 'dark' ? true : false); + const [animationAction, setAnimationAction] = useState(false); + const [chevronAnim, setChevronAnim] = useState(false); - const accountClickHandler = useCallback(() => { - setAccountDropdownOn(!isAccountDropdownOn); + const toggleAccountDropdown = useCallback(() => { + setChevronAnim(!chevronAnim); + useAnimation(isAccountDropdownOn, setAccountDropdownOn, setAnimationAction, 300); }, [setAccountDropdownOn, isAccountDropdownOn]); - const hideAccountDropdown = useCallback(() => { - setAccountDropdownOn(false); - }, [setAccountDropdownOn]); - - const faqClickHandler = useCallback(() => { - setFaqDropdownOn(!isFaqDropdownOn); + const toggleFaqDropdown = useCallback(() => { + useAnimation(isFaqDropdownOn, setFaqDropdownOn, setAnimationAction, 300); }, [setFaqDropdownOn, isFaqDropdownOn]); - const hideFaqDropdown = useCallback(() => { - setFaqDropdownOn(false); - }, [setFaqDropdownOn]); + const toggleDarkTheme = () => { + if (localStorage.getItem('theme') === 'dark') { + document.documentElement.removeAttribute('data-theme'); + localStorage.removeItem('theme'); + setDarkTheme(false); + } else { + localStorage.setItem('theme', 'dark'); + document.documentElement.setAttribute('data-theme', 'dark'); + setDarkTheme(true); + } + }; return (
@@ -51,82 +61,93 @@ const TopBar = (props: TopBarProps & ConnectedProps) => {
-
+
?
- - {isFaqDropdownOn && ( - - {props.user.name && (
-
+
{props.user.name}
-
+
- {isAccountDropdownOn && ( - - )} +
); diff --git a/frontend/control-center/src/components/Wrapper/index.module.scss b/frontend/control-center/src/components/Wrapper/index.module.scss index 05cdd3f2da..5496a588ec 100644 --- a/frontend/control-center/src/components/Wrapper/index.module.scss +++ b/frontend/control-center/src/components/Wrapper/index.module.scss @@ -13,7 +13,7 @@ .Content { width: auto; - background: white; + background: var(--color-background-white); padding: 32px; margin: 88px 2.5em 5em 7.5em; border-radius: 10px; diff --git a/frontend/control-center/src/pages/Catalog/CatalogItemList.tsx b/frontend/control-center/src/pages/Catalog/CatalogItemList.tsx index f72ee8d967..b09f6116c8 100644 --- a/frontend/control-center/src/pages/Catalog/CatalogItemList.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogItemList.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import ChannelCard from '../Channels/ChannelCard'; +import InfoCard, {InfoCardStyle} from '../Connectors/InfoCard'; import {StateModel} from '../../reducers'; import {useSelector} from 'react-redux'; import {useNavigate} from 'react-router-dom'; -import {SourceInfo} from './index'; +import {SourceInfo} from '../../components/SourceInfo'; import styles from './index.module.scss'; interface CatalogItemListProps { @@ -15,6 +15,7 @@ interface CatalogItemListProps { export const CatalogItemList = (props: CatalogItemListProps) => { const {list, installedConnectors, setDisplayDialogFromSource} = props; const config = useSelector((state: StateModel) => state.data.config); + const navigate = useNavigate(); return ( @@ -23,13 +24,14 @@ export const CatalogItemList = (props: CatalogItemListProps) => {
{list.map(infoItem => ( - { if (config.components[infoItem.configKey] && config.components[infoItem.configKey].enabled) { - navigate(infoItem.newChannelRoute); + installedConnectors ? navigate(infoItem.channelsListRoute) : navigate(infoItem.newChannelRoute); } else { setDisplayDialogFromSource(infoItem.type); } diff --git a/frontend/control-center/src/pages/Channels/ChannelsOutlet.tsx b/frontend/control-center/src/pages/Catalog/CatalogOutlet.tsx similarity index 60% rename from frontend/control-center/src/pages/Channels/ChannelsOutlet.tsx rename to frontend/control-center/src/pages/Catalog/CatalogOutlet.tsx index 32e6d8a4a6..81e684b2b0 100644 --- a/frontend/control-center/src/pages/Channels/ChannelsOutlet.tsx +++ b/frontend/control-center/src/pages/Catalog/CatalogOutlet.tsx @@ -1,8 +1,8 @@ import React from 'react'; import {Outlet} from 'react-router-dom'; -const ChannelsOutlet = () => { +const CatalogOutlet = () => { return ; }; -export default ChannelsOutlet; +export default CatalogOutlet; diff --git a/frontend/control-center/src/pages/Catalog/index.module.scss b/frontend/control-center/src/pages/Catalog/index.module.scss index 84da70835f..fe36c7b78e 100644 --- a/frontend/control-center/src/pages/Catalog/index.module.scss +++ b/frontend/control-center/src/pages/Catalog/index.module.scss @@ -2,7 +2,8 @@ @import 'assets/scss/colors.scss'; .catalogWrapper { - background: white; + background: var(--color-background-white); + color: var(--color-text-contrast); border-radius: 10px; padding: 32px; margin: 88px 1.5em 0 191px; diff --git a/frontend/control-center/src/pages/Catalog/index.tsx b/frontend/control-center/src/pages/Catalog/index.tsx index 6674b237c0..1059152d3a 100644 --- a/frontend/control-center/src/pages/Catalog/index.tsx +++ b/frontend/control-center/src/pages/Catalog/index.tsx @@ -1,161 +1,39 @@ import React, {useEffect, useState} from 'react'; - -import {ReactComponent as AiryAvatarIcon} from 'assets/images/icons/airyLogo.svg'; -import {ReactComponent as MessengerAvatarIcon} from 'assets/images/icons/facebookMessengerLogoBlue.svg'; -import {ReactComponent as SMSAvatarIcon} from 'assets/images/icons/phoneIcon.svg'; -import {ReactComponent as WhatsAppAvatarIcon} from 'assets/images/icons/whatsappLogoFilled.svg'; -import {ReactComponent as GoogleAvatarIcon} from 'assets/images/icons/googleLogo.svg'; -import {ReactComponent as InstagramIcon} from 'assets/images/icons/instagramLogoFilled.svg'; - import styles from './index.module.scss'; -import { - cyChannelsChatPluginAddButton, - cyChannelsChatPluginList, - cyChannelsFacebookAddButton, - cyChannelsFacebookList, - cyChannelsGoogleAddButton, - cyChannelsGoogleList, - cyChannelsTwilioSmsAddButton, - cyChannelsTwilioSmsList, - cyChannelsTwilioWhatsappAddButton, - cyChannelsTwilioWhatsappList, - cyChannelsInstagramAddButton, - cyChannelsInstagramList, -} from 'handles'; -import { - CHANNELS_FACEBOOK_ROUTE, - CHANNELS_TWILIO_SMS_ROUTE, - CHANNELS_TWILIO_WHATSAPP_ROUTE, - CHANNELS_CONNECTED_ROUTE, - CHANNELS_CHAT_PLUGIN_ROUTE, - CHANNELS_GOOGLE_ROUTE, - CHANNELS_INSTAGRAM_ROUTE, -} from '../../routes/routes'; import {StateModel} from '../../reducers'; import {useSelector} from 'react-redux'; import {allChannelsConnected} from '../../selectors/channels'; -import {FacebookMessengerRequirementsDialog} from '../Channels/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog'; -import {GoogleBusinessMessagesRequirementsDialog} from '../Channels/Providers/Google/GoogleBusinessMessagesRequirementsDialog'; -import {TwilioRequirementsDialog} from '../Channels/Providers/Twilio/TwilioRequirementsDialog'; -import {InstagramRequirementsDialog} from '../Channels/Providers/Instagram/InstagramRequirementsDialog'; +import {FacebookMessengerRequirementsDialog} from '../Connectors/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog'; +import {GoogleBusinessMessagesRequirementsDialog} from '../Connectors/Providers/Google/GoogleBusinessMessagesRequirementsDialog'; +import {TwilioRequirementsDialog} from '../Connectors/Providers/Twilio/TwilioRequirementsDialog'; +import {InstagramRequirementsDialog} from '../Connectors/Providers/Instagram/InstagramRequirementsDialog'; import {setPageTitle} from '../../services/pageTitle'; import {CatalogItemList} from './CatalogItemList'; import {Channel, Source} from 'model'; - -export type SourceInfo = { - type: Source; - title: string; - description: string; - image: JSX.Element; - newChannelRoute: string; - channelsListRoute: string; - configKey: string; - channelsToShow: number; - itemInfoString: string; - dataCyAddChannelButton: string; - dataCyChannelList: string; -}; - -const SourcesInfo: SourceInfo[] = [ - { - type: Source.chatPlugin, - title: 'Airy Live Chat', - description: 'Best of class browser messenger', - image: , - newChannelRoute: CHANNELS_CHAT_PLUGIN_ROUTE + '/new', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/chatplugin', - configKey: 'sources-chat-plugin', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsChatPluginAddButton, - dataCyChannelList: cyChannelsChatPluginList, - }, - { - type: Source.facebook, - title: 'Messenger', - description: 'Connect multiple Facebook pages', - image: , - newChannelRoute: CHANNELS_FACEBOOK_ROUTE + '/new', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/facebook', - configKey: 'sources-facebook', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsFacebookAddButton, - dataCyChannelList: cyChannelsFacebookList, - }, - { - type: Source.twilioSMS, - title: 'SMS', - description: 'Deliver SMS with ease', - image: , - newChannelRoute: CHANNELS_TWILIO_SMS_ROUTE + '/new_account', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/twilio.sms/#', - configKey: 'sources-twilio', - channelsToShow: 2, - itemInfoString: 'phones', - dataCyAddChannelButton: cyChannelsTwilioSmsAddButton, - dataCyChannelList: cyChannelsTwilioSmsList, - }, - { - type: Source.twilioWhatsApp, - title: 'WhatsApp', - description: 'World #1 chat app', - image: , - newChannelRoute: CHANNELS_TWILIO_WHATSAPP_ROUTE + '/new_account', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/twilio.whatsapp/#', - configKey: 'sources-twilio', - channelsToShow: 2, - itemInfoString: 'phones', - dataCyAddChannelButton: cyChannelsTwilioWhatsappAddButton, - dataCyChannelList: cyChannelsTwilioWhatsappList, - }, - { - type: Source.google, - title: 'Google Business Messages', - description: 'Be there when people search', - image: , - newChannelRoute: CHANNELS_GOOGLE_ROUTE + '/new_account', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/google', - configKey: 'sources-google', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsGoogleAddButton, - dataCyChannelList: cyChannelsGoogleList, - }, - { - type: Source.instagram, - title: 'Instagram', - description: 'Connect multiple Instagram pages', - image: , - newChannelRoute: CHANNELS_INSTAGRAM_ROUTE + '/new', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/instagram', - configKey: 'sources-facebook', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsInstagramAddButton, - dataCyChannelList: cyChannelsInstagramList, - }, -]; +import {getSourcesInfo, SourceInfo} from '../../components/SourceInfo'; const Catalog = () => { const channels = useSelector((state: StateModel) => Object.values(allChannelsConnected(state))); const [displayDialogFromSource, setDisplayDialogFromSource] = useState(''); const [notInstalledConnectors, setNotInstalledConnectors] = useState([]); const [installedConnectors, setInstalledConnectors] = useState([]); + const [sourcesInfo, setSourcesInfo] = useState([]); + const pageTitle = 'Catalog'; useEffect(() => { - setPageTitle('Catalog'); + setPageTitle(pageTitle); + setSourcesInfo(getSourcesInfo(pageTitle)); }, []); useEffect(() => { - SourcesInfo.map((infoItem: SourceInfo) => { + sourcesInfo.map((infoItem: SourceInfo) => { if (channelsBySource(infoItem.type).length === 0) { setNotInstalledConnectors(prevArr => [...prevArr, infoItem]); } else { setInstalledConnectors(prevArr => [...prevArr, infoItem]); } }); - }, []); + }, [sourcesInfo]); const OpenRequirementsDialog = ({source}: {source: string}): JSX.Element => { switch (source) { diff --git a/frontend/control-center/src/pages/Channels/ChannelCard/index.tsx b/frontend/control-center/src/pages/Channels/ChannelCard/index.tsx deleted file mode 100644 index f3ab33b48c..0000000000 --- a/frontend/control-center/src/pages/Channels/ChannelCard/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import {SourceInfo} from '..'; -import styles from './index.module.scss'; - -type SourceDescriptionCardProps = { - sourceInfo: SourceInfo; - addChannelAction: () => void; - installed: boolean; -}; - -const ChannelCard = (props: SourceDescriptionCardProps) => { - const {sourceInfo, addChannelAction, installed} = props; - - return ( -
-
-
{sourceInfo.image}
-
-

{sourceInfo.title}

- {!installed &&

{sourceInfo.description}

} -
-
-
- ); -}; - -export default ChannelCard; diff --git a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/index.tsx b/frontend/control-center/src/pages/Channels/ConnectedChannelsList/index.tsx deleted file mode 100644 index 01a09251f7..0000000000 --- a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/index.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import {useSelector} from 'react-redux'; -import {Link, useNavigate, useParams} from 'react-router-dom'; -import {sortBy} from 'lodash-es'; - -import {StateModel} from '../../../reducers'; -import {allChannels} from '../../../selectors/channels'; - -import {Channel, Source} from 'model'; -import ChannelsListItem from './ChannelsListItem'; -import {SearchField} from 'components'; -import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/arrowLeft.svg'; -import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; -import {ReactComponent as PlusIcon} from 'assets/images/icons/plus.svg'; -import {ReactComponent as CloseIcon} from 'assets/images/icons/close.svg'; - -import styles from './index.module.scss'; -import {cyChannelsFormBackButton} from 'handles'; -import { - CHANNELS_FACEBOOK_ROUTE, - CHANNELS_CHAT_PLUGIN_ROUTE, - CHANNELS_ROUTE, - CHANNELS_TWILIO_SMS_ROUTE, - CHANNELS_TWILIO_WHATSAPP_ROUTE, - CHANNELS_GOOGLE_ROUTE, - CHANNELS_INSTAGRAM_ROUTE, -} from '../../../routes/routes'; - -const ConnectedChannelsList = () => { - const {source} = useParams(); - const navigate = useNavigate(); - const channels = useSelector((state: StateModel) => { - return Object.values(allChannels(state)).filter((channel: Channel) => channel.source === source); - }); - - const [name, setName] = useState(''); - const [path, setPath] = useState(''); - const [searchText, setSearchText] = useState(''); - const [showingSearchField, setShowingSearchField] = useState(false); - - const filteredChannels = channels.filter((channel: Channel) => - channel.metadata?.name?.toLowerCase().includes(searchText.toLowerCase()) - ); - - useEffect(() => { - setPageTitle(); - }, [source, channels]); - - const setPageTitle = () => { - switch (source) { - case Source.facebook: - setName('Facebook Messenger'); - setPath(CHANNELS_FACEBOOK_ROUTE + '/new'); - break; - case Source.google: - setName('Google Business Messages'); - setPath(CHANNELS_GOOGLE_ROUTE + '/new_account'); - break; - case Source.twilioSMS: - setName('Twilio SMS'); - setPath(CHANNELS_TWILIO_SMS_ROUTE + '/new_account'); - break; - case Source.twilioWhatsApp: - setName('Twilio Whatsapp'); - setPath(CHANNELS_TWILIO_WHATSAPP_ROUTE + '/new_account'); - break; - case Source.chatPlugin: - setName('Chat Plugin'); - setPath(CHANNELS_CHAT_PLUGIN_ROUTE + '/new'); - break; - case Source.instagram: - setName('Instagram'); - setPath(CHANNELS_INSTAGRAM_ROUTE + '/new'); - break; - } - }; - - const showSearchFieldToggle = () => { - setShowingSearchField(!showingSearchField); - setSearchText(''); - }; - - return ( -
-
-

{name}

-
-
- {showingSearchField && ( - setSearchText(value)} - autoFocus={true} - resetClicked={() => setSearchText('')} - /> - )} -
-
- - -
-
-
- - - - Back to channels - - -
- {filteredChannels.length > 0 ? ( - sortBy(filteredChannels, (channel: Channel) => channel.metadata.name.toLowerCase()).map( - (channel: Channel) => ( -
- -
- ) - ) - ) : ( -
-

Result not found.

-

Try to search for a different term.

-
- )} -
-
- ); -}; - -export default ConnectedChannelsList; diff --git a/frontend/control-center/src/pages/Channels/index.tsx b/frontend/control-center/src/pages/Channels/index.tsx deleted file mode 100644 index 54043ffaab..0000000000 --- a/frontend/control-center/src/pages/Channels/index.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React, {useEffect} from 'react'; - -import {Channel, Source} from 'model'; -import ChannelCard from './ChannelCard'; - -import {ReactComponent as AiryAvatarIcon} from 'assets/images/icons/airyLogo.svg'; -import {ReactComponent as MessengerAvatarIcon} from 'assets/images/icons/facebookMessengerLogoBlue.svg'; -import {ReactComponent as SMSAvatarIcon} from 'assets/images/icons/phoneIcon.svg'; -import {ReactComponent as WhatsAppAvatarIcon} from 'assets/images/icons/whatsappLogoFilled.svg'; -import {ReactComponent as GoogleAvatarIcon} from 'assets/images/icons/googleLogo.svg'; -import {ReactComponent as InstagramIcon} from 'assets/images/icons/instagramLogoFilled.svg'; - -import styles from './index.module.scss'; -import { - cyChannelsChatPluginAddButton, - cyChannelsChatPluginList, - cyChannelsFacebookAddButton, - cyChannelsFacebookList, - cyChannelsGoogleAddButton, - cyChannelsGoogleList, - cyChannelsTwilioSmsAddButton, - cyChannelsTwilioSmsList, - cyChannelsTwilioWhatsappAddButton, - cyChannelsTwilioWhatsappList, - cyChannelsInstagramAddButton, - cyChannelsInstagramList, -} from 'handles'; -import { - CHANNELS_FACEBOOK_ROUTE, - CHANNELS_TWILIO_SMS_ROUTE, - CHANNELS_TWILIO_WHATSAPP_ROUTE, - CHANNELS_CONNECTED_ROUTE, - CHANNELS_CHAT_PLUGIN_ROUTE, - CHANNELS_GOOGLE_ROUTE, - CHANNELS_INSTAGRAM_ROUTE, -} from '../../routes/routes'; -import {StateModel} from '../../reducers'; -import {connect, ConnectedProps, useSelector} from 'react-redux'; -import {useNavigate} from 'react-router-dom'; -import {allChannelsConnected} from '../../selectors/channels'; -import {listChannels} from '../../actions/channel'; -import {setPageTitle} from '../../services/pageTitle'; - -export type SourceInfo = { - type: Source; - title: string; - description: string; - image: JSX.Element; - newChannelRoute: string; - channelsListRoute: string; - configKey: string; - channelsToShow: number; - itemInfoString: string; - dataCyAddChannelButton: string; - dataCyChannelList: string; -}; - -const SourcesInfo: SourceInfo[] = [ - { - type: Source.chatPlugin, - title: 'Airy Live Chat', - description: 'Best of class browser messenger', - image: , - newChannelRoute: CHANNELS_CHAT_PLUGIN_ROUTE + '/new', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/chatplugin', - configKey: 'sources-chat-plugin', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsChatPluginAddButton, - dataCyChannelList: cyChannelsChatPluginList, - }, - { - type: Source.facebook, - title: 'Messenger', - description: 'Connect multiple Facebook pages', - image: , - newChannelRoute: CHANNELS_FACEBOOK_ROUTE + '/new', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/facebook', - configKey: 'sources-facebook', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsFacebookAddButton, - dataCyChannelList: cyChannelsFacebookList, - }, - { - type: Source.twilioSMS, - title: 'SMS', - description: 'Deliver SMS with ease', - image: , - newChannelRoute: CHANNELS_TWILIO_SMS_ROUTE + '/new_account', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/twilio.sms/#', - configKey: 'sources-twilio', - channelsToShow: 2, - itemInfoString: 'phones', - dataCyAddChannelButton: cyChannelsTwilioSmsAddButton, - dataCyChannelList: cyChannelsTwilioSmsList, - }, - { - type: Source.twilioWhatsApp, - title: 'WhatsApp', - description: 'World #1 chat app', - image: , - newChannelRoute: CHANNELS_TWILIO_WHATSAPP_ROUTE + '/new_account', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/twilio.whatsapp/#', - configKey: 'sources-twilio', - channelsToShow: 2, - itemInfoString: 'phones', - dataCyAddChannelButton: cyChannelsTwilioWhatsappAddButton, - dataCyChannelList: cyChannelsTwilioWhatsappList, - }, - { - type: Source.google, - title: 'Google Business Messages', - description: 'Be there when people search', - image: , - newChannelRoute: CHANNELS_GOOGLE_ROUTE + '/new_account', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/google', - configKey: 'sources-google', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsGoogleAddButton, - dataCyChannelList: cyChannelsGoogleList, - }, - { - type: Source.instagram, - title: 'Instagram', - description: 'Connect multiple Instagram pages', - image: , - newChannelRoute: CHANNELS_INSTAGRAM_ROUTE + '/new', - channelsListRoute: CHANNELS_CONNECTED_ROUTE + '/instagram', - configKey: 'sources-facebook', - channelsToShow: 4, - itemInfoString: 'channels', - dataCyAddChannelButton: cyChannelsInstagramAddButton, - dataCyChannelList: cyChannelsInstagramList, - }, -]; - -const mapDispatchToProps = { - listChannels, -}; - -const mapStateToProps = (state: StateModel) => ({ - channels: Object.values(allChannelsConnected(state)), -}); - -const connector = connect(mapStateToProps, mapDispatchToProps); - -const Channels = (props: ConnectedProps) => { - const channels = useSelector((state: StateModel) => Object.values(allChannelsConnected(state))); - const channelsBySource = (Source: Source) => channels.filter((channel: Channel) => channel.source === Source); - const navigate = useNavigate(); - - useEffect(() => { - if (props.channels.length === 0) { - props.listChannels(); - } - setPageTitle('Connectors'); - }, [props.channels.length]); - - return ( -
-
-
-

Connectors

-
-
- -
- {SourcesInfo.map((infoItem: SourceInfo) => { - return ( - channelsBySource(infoItem.type).length > 0 && ( -
- { - navigate(infoItem.channelsListRoute); - }} - /> -
- ) - ); - })} -
-
- ); -}; - -export default connector(Channels); diff --git a/frontend/control-center/src/pages/Connectors/ChannelCard/index.module.scss b/frontend/control-center/src/pages/Connectors/ChannelCard/index.module.scss new file mode 100644 index 0000000000..7f7af8cac2 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ChannelCard/index.module.scss @@ -0,0 +1,65 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; +@import 'assets/scss/z-index.scss'; + +.container { + width: 260px; + height: 100px; + margin-bottom: 28px; + margin-right: 36px; + display: flex; + align-items: center; + border: 1px solid var(--color-dark-elements-gray); + border-radius: 10px; + background-color: var(--color-background-blue); + text-decoration: none; + &:hover { + border: 2px solid var(--color-airy-blue); + margin-left: -1px; + width: 261px; + cursor: pointer; + } +} + +.channelCard { + display: flex; + flex-direction: column; + justify-content: space-between; + margin-left: 16px; +} + +.logoTitleContainer { + @include font-base; + font-weight: 600; + margin-bottom: 13px; + color: var(--color-text-contrast); + display: flex; + align-items: center; + margin-top: 14px; + + svg { + height: 30px; + width: 30px; + margin-right: 10px; + } +} + +.linkContainer { + @include font-s; + display: flex; + align-items: center; + flex-direction: row; + align-items: center; + margin-bottom: 14px; + + span { + color: var(--color-airy-blue); + margin-right: 4px; + text-decoration: underline; + } + .arrowIcon { + width: 8px; + padding-top: 3px; + color: var(--color-airy-blue); + } +} diff --git a/frontend/control-center/src/pages/Connectors/ChannelCard/index.tsx b/frontend/control-center/src/pages/Connectors/ChannelCard/index.tsx new file mode 100644 index 0000000000..8d3556b48d --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ChannelCard/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styles from './index.module.scss'; +import {SourceInfo} from '../../../components/SourceInfo'; +import {Link} from 'react-router-dom'; +import {ReactComponent as ArrowRightIcon} from 'assets/images/icons/arrowRight.svg'; + +type ChannelCardProps = { + sourceInfo: SourceInfo; + channelsToShow?: number; +}; + +export const ChannelCard = (props: ChannelCardProps) => { + const {sourceInfo, channelsToShow} = props; + return ( + +
+
+ {sourceInfo.image} + {sourceInfo.title} +
+
+ + {channelsToShow} {channelsToShow === 1 ? 'channel' : 'channels'} + + +
+
+ + ); +}; diff --git a/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/ChannelsListItem/index.module.scss b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/ChannelsListItem/index.module.scss new file mode 100644 index 0000000000..cbaecdc694 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/ChannelsListItem/index.module.scss @@ -0,0 +1,236 @@ +@import 'assets/scss/fonts'; +@import 'assets/scss/colors'; + +.channelItem { + display: flex; + flex-direction: row; + align-items: center; + flex-grow: 1; + height: 64px; + border-bottom: 1px solid var(--color-light-gray); +} + +.channelLogo { + display: flex; + align-items: center; + height: 40px; + width: 40px; + margin-right: 8px; +} + +.channelNameButton { + display: flex; + flex: 1 1; + border-bottom: 1px solid var(--color-light-gray); +} + +.container { + display: flex; +} + +.channelLogoWrapper { + margin-right: 4px; + flex: none; + width: 24px; + height: 24px; + + svg { + width: 24px; + height: 24px; + } +} + +.channelName { + height: 50px; + display: flex; + align-items: center; + padding-left: 16px; + padding-right: 16px; + color: var(--color-text-gray); + &:empty { + padding-right: 0px; + } +} + +.channelId { + height: 50px; + display: flex; + align-items: center; +} + +.imageUrlLogo { + width: 35px; + height: 35px; + border-radius: 50%; +} + +.connectedHint { + display: inline-flex; + flex-direction: row; + border: 1px solid var(--color-soft-green); + padding: 0 8px; + border-radius: 4px; + margin-left: 8px; + width: 110px; + + @include font-s; + text-transform: uppercase; + color: var(--color-soft-green); + svg { + position: relative; + top: -1px; + left: 3px; + height: 8px; + width: auto; + margin-top: 5px; + } +} + +.listButtons { + display: flex; + align-items: center; + height: 50px; + position: absolute; + right: 44px; + svg { + color: var(--color-text-gray); + &:hover { + color: var(--color-airy-blue); + } + } +} + +.disabledDisconnect { + width: 20%; + display: flex; + height: 60px; + align-items: center; + justify-content: flex-end; + margin: 0 4px; + padding: 0 16px; +} + +.channelConnect { + width: 20%; + display: flex; + height: 60px; + align-items: center; + justify-content: flex-end; +} + +.connectButton button { + margin: 0.85em 0; + padding: 0 0.5em; + color: #fff; + background: var(--color-airy-blue); + + &:hover { + color: #fff; + } + text-decoration: none; +} + +header { + display: flex; + justify-content: space-between; + padding: 0.25em 1.25em; + div { + align-self: center; + } +} + +.closeIcon { + padding-right: 5px; + svg path { + fill: red; + } +} + +.errorContainer { + display: flex; + align-items: center; + box-sizing: border-box; + background: #ebf7ff; + border: 1px solid var(--color-light-gray); + border-radius: 4px; + padding: 6px; + margin-top: 24px; + svg { + fill: #a3afb6; + padding-right: 8px; + padding-left: 4px; + padding-top: 2px; + } +} + +:global { + .add-source span { + opacity: 1 !important; + } + .channelsConnect { + @include font-m; + + background-color: var(--color-airy-blue); + color: var(--color-background-white); + border-radius: 4px; + text-align: center; + height: 48px; + border: none; + cursor: pointer; + padding: 8px 16px; + margin: 0 0; + + &:hover { + background-color: var(--color-airy-blue-hover); + } + + &:active { + background: var(--color-airy-blue-pressed); + } + + &:disabled { + cursor: not-allowed; + color: var(--color-light-gray); + background-color: var(--color-dark-elements-gray) !important; + } + + &:focus { + outline: none !important; + } + + &::-moz-focus-inner { + border: 0; + } + } +} + +.disconnectModal { + p { + color: var(--color-text-contrast); + font-family: Lato; + font-size: 16px; + letter-spacing: 0; + line-height: 24px; + margin-bottom: 16px; + } + a { + font-weight: 400; + } +} + +.modalSeparator { + margin-top: 24px; + margin-bottom: 24px; + height: 1px; + background: var(--color-light-gray); +} + +.modalButtons { + height: 24px; + display: flex; + justify-content: center; + align-items: center; + button { + margin: 0 8px; + } +} diff --git a/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/ChannelsListItem/index.tsx b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/ChannelsListItem/index.tsx new file mode 100644 index 0000000000..b8e44f92ba --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/ChannelsListItem/index.tsx @@ -0,0 +1,107 @@ +import React, {useState} from 'react'; +import {connect, ConnectedProps} from 'react-redux'; + +import {disconnectChannel} from '../../../../actions/channel'; + +import {SettingsModal, Button} from 'components'; +import {Channel} from 'model'; + +import {ReactComponent as CheckMarkFilledIcon} from 'assets/images/icons/checkmarkFilled.svg'; +import {ReactComponent as PencilIcon} from 'assets/images/icons/pencil.svg'; +import {ReactComponent as DisconnectIcon} from 'assets/images/icons/disconnectIcon.svg'; + +import styles from './index.module.scss'; +import {useNavigate} from 'react-router-dom'; + +type ChannelListItemProps = { + channel: Channel; +} & ConnectedProps; + +const mapDispatchToProps = { + disconnectChannel, +}; + +const connector = connect(null, mapDispatchToProps); + +const ChannelListItem = (props: ChannelListItemProps) => { + const {channel} = props; + const navigate = useNavigate(); + const [deletePopupVisible, setDeletePopupVisible] = useState(false); + const path = location.pathname.includes('connectors') ? 'connectors' : 'catalog'; + + const togglePopupVisibility = () => { + setDeletePopupVisible(!deletePopupVisible); + }; + + const isPhoneNumberSource = () => { + return channel.source === 'twilio.sms' || channel.source === 'twilio.whatsapp'; + }; + + const disconnectChannel = () => { + props.disconnectChannel({ + source: channel.source, + channelId: channel.id, + }); + togglePopupVisibility(); + }; + + return ( + <> +
+
+ {channel.connected && } +
{channel.metadata?.name}
+ {isPhoneNumberSource() &&
{channel.sourceChannelId}
} +
+ + +
+
+
+ + {deletePopupVisible && ( + +
+

+ You are about to disconnect a channel. You will not receive any new messages in Airy or be able to send + messages anymore. +

+

+ If you need help or experience a problem, please reach out to{' '} + support@airy.co. +

+
+
+ + +
+
+ + )} + + ); +}; + +export default connector(ChannelListItem); diff --git a/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/DisableModal/index.module.scss b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/DisableModal/index.module.scss new file mode 100644 index 0000000000..cb656fba17 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/DisableModal/index.module.scss @@ -0,0 +1,64 @@ +@import 'assets/scss/colors.scss'; +@import 'assets/scss/fonts.scss'; + +.container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + min-height: 520px; + min-width: 1000px; + color: var(--color-text-gray); + background: white; + + h1 { + @include font-xl; + color: var(--color-text-contrast); + font-weight: 800; + margin-top: 50px; + margin-bottom: 16px; + } + p { + @include font-m; + color: var(--color-text-contrast); + font-weight: 400; + margin-bottom: 50px; + max-width: 75%; + line-break: anywhere; + text-align: center; + } +} + +.buttonContainer { + display: flex; + align-items: center; + button { + @include font-base; + margin: 0 12px; + } +} + +.errorMessage { + @include font-base; + position: absolute; + bottom: 40px; + color: var(--color-red-alert); + font-size: 16px; +} + +@keyframes spinAnimationRefresh { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(7200deg); + } +} + +.spinAnimation { + svg { + height: 20px; + width: 20px; + animation: spinAnimationRefresh 40s linear; + } +} diff --git a/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/DisableModal/index.tsx b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/DisableModal/index.tsx new file mode 100644 index 0000000000..bb49b34f4e --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/DisableModal/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import styles from './index.module.scss'; + +import {ReactComponent as ErrorMessage} from 'assets/images/icons/errorMessage.svg'; +import {ReactComponent as RefreshIcon} from 'assets/images/icons/refreshIcon.svg'; +import {Button} from 'components/cta/Button'; + +type DisableModalProps = { + setConfirmDisable: (confirm: boolean) => void; + setCancelDisable: (cancel: boolean) => void; + channel: string; + channelLength: number; + isLoading: boolean; + error: boolean; +}; + +export const DisableModal = (props: DisableModalProps) => { + const {setConfirmDisable, setCancelDisable, channel, channelLength, isLoading, error} = props; + + const handleConfirm = () => { + setConfirmDisable(true); + }; + + const handleCancel = () => { + setCancelDisable(true); + }; + + return ( +
+ +

Disable Channels

+

+ Are you sure you want to disable
all {channelLength} {channelLength === 1 ? 'channel' : 'channels'} of{' '} + {channel}? +

+
+
+ + +
+
+ {error && Unable to disable Channel} +
+ ); +}; diff --git a/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/index.module.scss b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/index.module.scss new file mode 100644 index 0000000000..37a53073e8 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/index.module.scss @@ -0,0 +1,209 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; +@import 'assets/scss/animations.scss'; + +.wrapper { + background: var(--color-background-white); + color: var(--color-text-contrast); + display: block; + border-radius: 10px; + padding-left: 96px; + padding-top: 88px; + width: 100%; + padding: 32px; + margin: 88px 1.5em 0 191px; + min-height: calc(100vh - 170px); +} + +.headlineRow { + display: flex; + place-content: space-between; + margin-top: 32px; + margin-bottom: 16px; +} + +.headline { + @include font-xl; + font-weight: bold; +} + +.description { + @include font-base; + color: var(--color-text-gray); +} + +.linkButtonContainer { + @include font-base; + display: flex; + align-items: center; + color: var(--color-text-contrast); +} + +.backButton { + display: block; + margin-bottom: 16px; + cursor: pointer; + text-decoration: none; + max-width: 200px; + &:hover { + text-decoration: underline; + } +} + +.backIcon { + height: 20px; + width: 20px; + path { + fill: var(--color-text-contrast); + } + margin-right: 8px; +} + +.inputContainer { + display: flex; + flex-direction: column; + margin-bottom: 32px; + width: 474px; + margin-top: 16px; + color: var(--color-text-contrast); + + label { + margin-top: 24px; + } + + input { + @include font-base; + } +} + +.subtitle { + display: flex; + flex-direction: column; + @include font-s; + color: var(--color-airy-blue); + img { + width: 17px; + height: 13px; + } +} + +.connectedChannels { + display: flex; + flex-direction: column; + margin-bottom: 32px; + width: 474px; + margin-top: 16px; +} + +.channelRow { + display: flex; + flex-direction: row; +} + +.connectedHint { + display: inline-flex; + align-items: center; + flex-direction: row; + border: 1px solid var(--color-soft-green); + padding: 0 8px; + border-radius: 4px; + margin-left: 8px; + font-family: 'Lato', sans-serif; + font-size: 0.8rem; + line-height: 1rem; + text-transform: uppercase; + color: var(--color-soft-green); +} + +.buttons { + display: flex; + flex-direction: row; + align-items: center; + + button { + align-self: center; + height: 32px; + width: 32px; + margin-left: 16px; + border-radius: 32px; + background: var(--color-blue-white-button); + border: none; + cursor: pointer; + } + + button:hover { + background-color: var(--color-button-light-blue); + } + + button:last-child { + border-radius: 48px; + height: 32px; + width: 32px; + border-radius: 32px; + background: var(--color-airy-blue); + svg { + path { + fill: var(--color-background-white); + } + } + } + button:last-child:hover { + background-color: var(--color-airy-blue-hover); + } +} + +.searchFieldButtons { + display: flex; + align-items: center; +} + +.searchField { + min-width: 300px; + animation: searchFieldAnimation 3000ms ease; + height: 32px; + border-radius: 32px; +} + +@keyframes searchFieldAnimation { + 0% { + width: 0px; + opacity: 0; + } + + 100% { + width: 200px; + opacity: 1; + } +} + +.noSearchMatch { + @include font-m; + font-weight: bold; + color: var(--color-text-contrast); + margin-bottom: 4px; +} + +.closeIcon { + height: 10px; + width: 10px; + z-index: 2; +} + +.plusIcon { + height: 13px; + width: 13px; +} + +.searchIcon { + height: 18px; + width: 18px; + color: var(--color-text-gray); +} + +.animateIn { + animation: searchfieldAnimIn 300ms ease; +} + +.animateOut { + animation: searchfieldAnimOut 300ms ease; +} diff --git a/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/index.tsx b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/index.tsx new file mode 100644 index 0000000000..97a358f33a --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConnectedChannelsList/index.tsx @@ -0,0 +1,208 @@ +import React, {useLayoutEffect, useMemo, useState} from 'react'; +import {useSelector} from 'react-redux'; +import {useNavigate, useParams} from 'react-router-dom'; +import {sortBy} from 'lodash-es'; + +import {StateModel} from '../../../reducers'; +import {allChannels} from '../../../selectors/channels'; + +import {Channel, Source} from 'model'; + +import {SearchField, LinkButton} from 'components'; +import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/leftArrowCircle.svg'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {ReactComponent as PlusIcon} from 'assets/images/icons/plus.svg'; +import {ReactComponent as CloseIcon} from 'assets/images/icons/close.svg'; + +import styles from './index.module.scss'; +import {cyChannelsFormBackButton} from 'handles'; +import { + CONNECTORS_FACEBOOK_ROUTE, + CONNECTORS_CHAT_PLUGIN_ROUTE, + CONNECTORS_TWILIO_SMS_ROUTE, + CONNECTORS_TWILIO_WHATSAPP_ROUTE, + CONNECTORS_GOOGLE_ROUTE, + CONNECTORS_INSTAGRAM_ROUTE, + CATALOG_FACEBOOK_ROUTE, + CATALOG_CHAT_PLUGIN_ROUTE, + CATALOG_TWILIO_SMS_ROUTE, + CATALOG_TWILIO_WHATSAPP_ROUTE, + CATALOG_GOOGLE_ROUTE, + CATALOG_INSTAGRAM_ROUTE, +} from '../../../routes/routes'; +import {getChannelAvatar} from '../../../components/ChannelAvatar'; +import ChannelsListItem from './ChannelsListItem'; +import {Pagination} from '../../../components/Pagination'; +import {useAnimation} from 'render/services/useAnimation'; + +const ConnectedChannelsList = () => { + const {source} = useParams(); + const navigate = useNavigate(); + const channels = useSelector((state: StateModel) => { + return Object.values(allChannels(state)).filter((channel: Channel) => channel.source === source); + }); + + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [path, setPath] = useState(''); + const [searchText, setSearchText] = useState(''); + const [showingSearchField, setShowingSearchField] = useState(false); + const [animationAction, setAnimationAction] = useState(false); + + const filteredChannels = channels.filter((channel: Channel) => + channel.metadata?.name?.toLowerCase().includes(searchText.toLowerCase()) + ); + + const pageSize = filteredChannels.length >= 8 ? 8 : filteredChannels.length; + const [currentPage, setCurrentPage] = useState(1); + + const currentTableData = useMemo(() => { + const firstPageIndex = (currentPage - 1) * pageSize; + const lastPageIndex = firstPageIndex + pageSize; + return filteredChannels.slice(firstPageIndex, lastPageIndex); + }, [currentPage, pageSize]); + + useLayoutEffect(() => { + getInfo(); + }, [source, channels]); + + const getInfo = () => { + let ROUTE; + switch (source) { + case Source.facebook: + setName('Facebook Messenger'); + setDescription('Connect multiple Facebook pages'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_FACEBOOK_ROUTE : CATALOG_FACEBOOK_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.google: + setName('Google Business Messages'); + setDescription('Be there when people search'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_GOOGLE_ROUTE : CATALOG_GOOGLE_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.twilioSMS: + setName('Twilio SMS'); + setDescription('Deliver SMS with ease'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_TWILIO_SMS_ROUTE : CATALOG_TWILIO_SMS_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.twilioWhatsApp: + setName('Twilio Whatsapp'); + setDescription('World #1 chat app'); + ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_TWILIO_WHATSAPP_ROUTE + : CATALOG_TWILIO_WHATSAPP_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.chatPlugin: + setName('Chat Plugin'); + setDescription('Best of class browser messenger'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_CHAT_PLUGIN_ROUTE : CATALOG_CHAT_PLUGIN_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.instagram: + setName('Instagram'); + setDescription('Connect multiple Instagram pages'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_INSTAGRAM_ROUTE : CATALOG_INSTAGRAM_ROUTE; + setPath(ROUTE + '/new'); + break; + } + }; + + const showSearchFieldToggle = () => { + useAnimation(showingSearchField, setShowingSearchField, setAnimationAction, 300); + setSearchText(''); + }; + + return ( +
+ navigate(-1)} type="button"> +
+ + Channels +
+
+
+
+
{getChannelAvatar(source)}
+
+

{name}

+

{description}

+
+
+
+
+
+
+
+ {showingSearchField && ( + setSearchText(value)} + autoFocus={true} + style={{height: '32px', borderRadius: '32px'}} + resetClicked={() => setSearchText('')} + /> + )} +
+
+
+
+ + +
+
+
+ Name + Manage +
+
+ {filteredChannels.length > 0 ? ( + sortBy(searchText === '' ? currentTableData : filteredChannels, (channel: Channel) => + channel.metadata.name.toLowerCase() + ).map((channel: Channel) => ( +
+ +
+ )) + ) : ( +
+

Result not found.

+

Try to search for a different term.

+
+ )} +
+ = pageSize ? pageSize : filteredChannels.length} + currentPage={currentPage} + onPageChange={page => setCurrentPage(page)} + onSearch={searchText !== ''} + /> +
+ ); +}; + +export default ConnectedChannelsList; diff --git a/frontend/control-center/src/pages/Connectors/ConnectorsOutlet.tsx b/frontend/control-center/src/pages/Connectors/ConnectorsOutlet.tsx new file mode 100644 index 0000000000..cf6a6e7430 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/ConnectorsOutlet.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import {Outlet} from 'react-router-dom'; + +const ConnectorsOutlet = () => { + return ; +}; + +export default ConnectorsOutlet; diff --git a/frontend/control-center/src/pages/Connectors/EmptyStateConnectors/index.module.scss b/frontend/control-center/src/pages/Connectors/EmptyStateConnectors/index.module.scss new file mode 100644 index 0000000000..ff457f0e04 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/EmptyStateConnectors/index.module.scss @@ -0,0 +1,43 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.container { + display: flex; + flex-direction: column; + flex: 1; + align-items: center; + justify-content: center; + margin-top: 10%; + + h1 { + @include font-xl; + font-weight: 800; + margin-top: 32px; + margin-bottom: 32px; + } + + p { + @include font-m; + text-align: center; + } +} + +.searchIconContainer { + display: flex; + justify-content: center; + align-items: center; + height: 105px; + width: 105px; + background: var(--color-background-gray); +} + +.linkContainer { + @include font-m; + display: flex; + align-items: center; + color: var(--color-airy-blue); + text-decoration: none; + margin-right: 4px; + width: 95px; + justify-content: space-around; +} diff --git a/frontend/control-center/src/pages/Connectors/EmptyStateConnectors/index.tsx b/frontend/control-center/src/pages/Connectors/EmptyStateConnectors/index.tsx new file mode 100644 index 0000000000..cf1a7b96d5 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/EmptyStateConnectors/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {ReactComponent as CatalogIcon} from 'assets/images/icons/catalogIcon.svg'; +import {Link} from 'react-router-dom'; +import {CATALOG_ROUTE} from '../../../routes/routes'; + +export const EmptyStateConnectors = () => { + return ( +
+
+ +
+

No Connectors Found

+

You don't have any connectors installed, please open the

+
+ + + Catalog + +

and explore more.

+
+
+ ); +}; diff --git a/frontend/control-center/src/pages/Channels/ChannelCard/index.module.scss b/frontend/control-center/src/pages/Connectors/InfoCard/index.module.scss similarity index 70% rename from frontend/control-center/src/pages/Channels/ChannelCard/index.module.scss rename to frontend/control-center/src/pages/Connectors/InfoCard/index.module.scss index f6477a33f7..cf9c09abaa 100644 --- a/frontend/control-center/src/pages/Channels/ChannelCard/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/InfoCard/index.module.scss @@ -3,7 +3,8 @@ @import 'assets/scss/z-index.scss'; .channelCard { - width: 230px; + width: 260px; + height: 100px; margin-bottom: 28px; margin-right: 36px; display: flex; @@ -12,6 +13,9 @@ border-radius: 10px; background-color: var(--color-background-blue); &:hover { + border: 2px solid var(--color-airy-blue); + margin-left: -1px; + width: 261px; cursor: pointer; } } @@ -59,9 +63,6 @@ justify-content: center; border-radius: 10px; border: 2px solid transparent; - &:hover { - border: 2px solid var(--color-airy-blue); - } } .channelLogoTitleContainerInstalled { @@ -92,3 +93,41 @@ } } } + +.isExpandedCard { + height: 100px; + width: 260px; + &:hover { + border: 2px solid var(--color-airy-blue); + margin-left: -1px; + width: 261px; + } +} + +.isExpandedContainer { + height: 100px; + width: 260px; + padding: 6px 0 0 0; + flex-direction: column; + align-items: start; + &:hover { + width: 261px; + } +} + +.isExpandedLogo { + min-width: 40px; + max-width: 100%; + margin: 0 18px; +} + +.isExpandedDetails { + width: 100%; + margin-left: 18px; + h1 { + @include font-base; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/frontend/control-center/src/pages/Connectors/InfoCard/index.tsx b/frontend/control-center/src/pages/Connectors/InfoCard/index.tsx new file mode 100644 index 0000000000..ed4449eca8 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/InfoCard/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import {SourceInfo} from '../../../components/SourceInfo'; +import styles from './index.module.scss'; + +export enum InfoCardStyle { + normal = 'normal', + expanded = 'expanded', +} + +type InfoCardProps = { + sourceInfo: SourceInfo; + addChannelAction: () => void; + installed: boolean; + style: InfoCardStyle; +}; + +const InfoCard = (props: InfoCardProps) => { + const {sourceInfo, addChannelAction, installed, style} = props; + + return ( +
+
+
+ {sourceInfo.image} +
+
+

{sourceInfo.title}

+ {!installed &&

{sourceInfo.description}

} +
+
+
+ ); +}; + +export default InfoCard; diff --git a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/ChatPluginConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.module.scss similarity index 96% rename from frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/ChatPluginConnect.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.module.scss index 937e5c62a0..6d843d3420 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/ChatPluginConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.module.scss @@ -2,7 +2,8 @@ @import 'assets/scss/colors.scss'; .wrapper { - background: white; + background: var(--color-background-white); + color: var(--color-text-contrast); display: block; border-radius: 10px; padding-left: 96px; diff --git a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx similarity index 85% rename from frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx index 4c06e8811f..8408764c34 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/ChatPluginConnect.tsx @@ -18,7 +18,12 @@ import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/arrowLeft.svg import styles from './ChatPluginConnect.module.scss'; -import {CHANNELS_CHAT_PLUGIN_ROUTE, CHANNELS_CONNECTED_ROUTE} from '../../../../../routes/routes'; +import { + CONNECTORS_CHAT_PLUGIN_ROUTE, + CONNECTORS_CONNECTED_ROUTE, + CATALOG_CONNECTED_ROUTE, + CATALOG_CHAT_PLUGIN_ROUTE, +} from '../../../../../routes/routes'; const mapDispatchToProps = { connectChatPlugin, @@ -36,6 +41,12 @@ const connector = connect(mapStateToProps, mapDispatchToProps); const ChatPluginConnect = (props: ConnectedProps) => { const {channelId} = useParams(); const navigate = useNavigate(); + const CONNECTED_ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_CONNECTED_ROUTE + : CATALOG_CONNECTED_ROUTE; + const CHAT_PLUGIN_ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_CHAT_PLUGIN_ROUTE + : CATALOG_CHAT_PLUGIN_ROUTE; const createNewConnection = (displayName: string, imageUrl?: string) => { props @@ -46,13 +57,13 @@ const ChatPluginConnect = (props: ConnectedProps) => { }), }) .then(() => { - navigate(CHANNELS_CONNECTED_ROUTE + '/chatplugin', {replace: true}); + navigate(CONNECTED_ROUTE + '/chatplugin', {replace: true}); }); }; const updateConnection = (displayName: string, imageUrl?: string) => { props.updateChannel({channelId: channelId, name: displayName, imageUrl: imageUrl}).then(() => { - navigate(CHANNELS_CONNECTED_ROUTE + '/chatplugin', {replace: true}); + navigate(CONNECTED_ROUTE + '/chatplugin', {replace: true}); }); }; @@ -62,7 +73,7 @@ const ChatPluginConnect = (props: ConnectedProps) => { } }; - const openNewPage = () => navigate(CHANNELS_CHAT_PLUGIN_ROUTE + '/new'); + const openNewPage = () => navigate(CHAT_PLUGIN_ROUTE + '/new'); const OverviewSection = () => (
@@ -81,7 +92,7 @@ const ChatPluginConnect = (props: ConnectedProps) => {
{channel.metadata?.name}
- + Edit void; } export const ConnectNewChatPlugin = ({createNewConnection}: ConnectNewChatPluginProps) => { diff --git a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/CustomiseSection.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CustomiseSection.module.scss similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/CustomiseSection.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CustomiseSection.module.scss diff --git a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/CustomiseSection.tsx b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CustomiseSection.tsx similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/CustomiseSection.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/CustomiseSection.tsx diff --git a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/EditChatPlugin.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/EditChatPlugin.module.scss similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/EditChatPlugin.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/EditChatPlugin.module.scss diff --git a/frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/EditChatPlugin.tsx b/frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/EditChatPlugin.tsx similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Airy/ChatPlugin/sections/EditChatPlugin.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Airy/ChatPlugin/sections/EditChatPlugin.tsx diff --git a/frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.module.scss similarity index 92% rename from frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookConnect.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.module.scss index 65d35ec957..80e91d3b42 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.module.scss @@ -2,7 +2,7 @@ @import 'assets/scss/colors.scss'; .wrapper { - background: white; + background: var(--color-background-white); display: block; border-radius: 10px; padding-left: 96px; @@ -16,6 +16,7 @@ .headline { @include font-xl; font-weight: bold; + color: var(--color-text-contrast); margin-bottom: 8px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx similarity index 93% rename from frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookConnect.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx index cb3406e8b8..430af62a44 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookConnect.tsx @@ -9,7 +9,7 @@ import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/arrowLeft.svg import styles from './FacebookConnect.module.scss'; -import {CHANNELS_CONNECTED_ROUTE} from '../../../../../routes/routes'; +import {CONNECTORS_CONNECTED_ROUTE, CATALOG_CONNECTED_ROUTE} from '../../../../../routes/routes'; import {useCurrentChannel} from '../../../../../selectors/channels'; import {useNavigate} from 'react-router-dom'; @@ -30,6 +30,10 @@ const FacebookConnect = (props: ConnectedProps) => { const [buttonTitle, setButtonTitle] = useState('Connect Page'); const [errorMessage, setErrorMessage] = useState(''); + const CONNECTED_ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_CONNECTED_ROUTE + : CATALOG_CONNECTED_ROUTE; + const buttonStatus = () => { return !(id.length > 5 && token != ''); }; @@ -56,7 +60,7 @@ const FacebookConnect = (props: ConnectedProps) => { connectFacebookChannel(connectPayload) .then(() => { - navigate(CHANNELS_CONNECTED_ROUTE + '/facebook', {replace: true}); + navigate(CONNECTED_ROUTE + '/facebook', {replace: true}); }) .catch(() => { setErrorMessage('Please check entered value'); diff --git a/frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.module.scss similarity index 91% rename from frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.module.scss index c872e48ef6..7caea7a4b3 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.module.scss @@ -7,7 +7,7 @@ flex-direction: column; width: 582px; height: 430px; - background: white; + background: var(--color-background-white); border-radius: 8px; z-index: $popup; position: fixed; @@ -24,6 +24,7 @@ .headline { @include font-base; + color: var(--color-text-contrast); padding-top: 40px; padding-left: 40px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.tsx b/frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.tsx similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Facebook/Messenger/FacebookMessengerRequirementsDialog/index.tsx diff --git a/frontend/control-center/src/pages/Channels/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.module.scss similarity index 91% rename from frontend/control-center/src/pages/Channels/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.module.scss index 756bb3c014..69844dc9da 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.module.scss @@ -6,7 +6,7 @@ display: flex; flex-direction: column; width: 582px; - background: white; + background: var(--color-background-white); border-radius: 8px; z-index: $popup; position: fixed; @@ -23,6 +23,7 @@ .headline { @include font-base; + color: var(--color-text-contrast); padding-top: 40px; padding-left: 40px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.tsx b/frontend/control-center/src/pages/Connectors/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.tsx similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Google/GoogleBusinessMessagesRequirementsDialog/index.tsx diff --git a/frontend/control-center/src/pages/Channels/Providers/Google/GoogleConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Google/GoogleConnect.module.scss similarity index 92% rename from frontend/control-center/src/pages/Channels/Providers/Google/GoogleConnect.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Google/GoogleConnect.module.scss index 65d35ec957..f542613968 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Google/GoogleConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Google/GoogleConnect.module.scss @@ -2,7 +2,7 @@ @import 'assets/scss/colors.scss'; .wrapper { - background: white; + background: var(--color-background-white); display: block; border-radius: 10px; padding-left: 96px; @@ -15,6 +15,7 @@ .headline { @include font-xl; + color: var(--color-text-contrast); font-weight: bold; margin-bottom: 8px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Google/GoogleConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Google/GoogleConnect.tsx similarity index 92% rename from frontend/control-center/src/pages/Channels/Providers/Google/GoogleConnect.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Google/GoogleConnect.tsx index 0504091865..ecadbbeb3c 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Google/GoogleConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Google/GoogleConnect.tsx @@ -9,7 +9,7 @@ import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/arrowLeft.svg import styles from './GoogleConnect.module.scss'; -import {CHANNELS_CONNECTED_ROUTE} from '../../../../routes/routes'; +import {CONNECTORS_CONNECTED_ROUTE, CATALOG_CONNECTED_ROUTE} from '../../../../routes/routes'; import {useCurrentChannel} from '../../../../selectors/channels'; import {useNavigate} from 'react-router-dom'; @@ -29,6 +29,10 @@ const GoogleConnect = (props: ConnectedProps) => { const [buttonTitle, setButtonTitle] = useState('Connect Page'); const [errorMessage, setErrorMessage] = useState(''); + const CONNECTED_ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_CONNECTED_ROUTE + : CATALOG_CONNECTED_ROUTE; + const buttonStatus = () => { return !(id.length > 5 && name.length > 0); }; @@ -51,7 +55,7 @@ const GoogleConnect = (props: ConnectedProps) => { connectGoogleChannel(connectPayload) .then(() => { - navigate(CHANNELS_CONNECTED_ROUTE + '/google', {replace: true}); + navigate(CONNECTED_ROUTE + '/google', {replace: true}); }) .catch(() => { setErrorMessage('Please check entered value'); diff --git a/frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramConnect.module.scss similarity index 92% rename from frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramConnect.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramConnect.module.scss index 65d35ec957..f542613968 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramConnect.module.scss @@ -2,7 +2,7 @@ @import 'assets/scss/colors.scss'; .wrapper { - background: white; + background: var(--color-background-white); display: block; border-radius: 10px; padding-left: 96px; @@ -15,6 +15,7 @@ .headline { @include font-xl; + color: var(--color-text-contrast); font-weight: bold; margin-bottom: 8px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramConnect.tsx similarity index 94% rename from frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramConnect.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramConnect.tsx index bfe0ddbe0e..928ba27801 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramConnect.tsx @@ -9,7 +9,7 @@ import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/arrowLeft.svg import styles from './InstagramConnect.module.scss'; -import {CHANNELS_CONNECTED_ROUTE} from '../../../../routes/routes'; +import {CONNECTORS_CONNECTED_ROUTE, CATALOG_CONNECTED_ROUTE} from '../../../../routes/routes'; import {useCurrentChannel} from '../../../../selectors/channels'; import {useNavigate} from 'react-router-dom'; @@ -31,6 +31,10 @@ const InstagramConnect = (props: ConnectedProps) => { const [buttonTitle, setButtonTitle] = useState('Connect Page'); const [errorMessage, setErrorMessage] = useState(''); + const CONNECTED_ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_CONNECTED_ROUTE + : CATALOG_CONNECTED_ROUTE; + const buttonStatus = () => { return !(id.length > 5 && token != ''); }; @@ -58,7 +62,7 @@ const InstagramConnect = (props: ConnectedProps) => { connectInstagramChannel(connectPayload) .then(() => { - navigate(CHANNELS_CONNECTED_ROUTE + '/instagram', {replace: true}); + navigate(CONNECTED_ROUTE + '/instagram', {replace: true}); }) .catch(() => { setErrorMessage('Please check entered value'); diff --git a/frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramRequirementsDialog/index.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramRequirementsDialog/index.module.scss similarity index 91% rename from frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramRequirementsDialog/index.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramRequirementsDialog/index.module.scss index 4dd5875522..ae0d2cafa0 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramRequirementsDialog/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramRequirementsDialog/index.module.scss @@ -6,7 +6,7 @@ flex-direction: column; width: 582px; height: 430px; - background: white; + background: var(--color-background-white); border-radius: 8px; z-index: 1; position: fixed; @@ -23,6 +23,7 @@ .headline { @include font-base; + color: var(--color-text-contrast); padding-top: 40px; padding-left: 40px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramRequirementsDialog/index.tsx b/frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramRequirementsDialog/index.tsx similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Instagram/InstagramRequirementsDialog/index.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Instagram/InstagramRequirementsDialog/index.tsx diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/SMS/TwilioSmsConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Twilio/SMS/TwilioSmsConnect.module.scss similarity index 87% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/SMS/TwilioSmsConnect.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/SMS/TwilioSmsConnect.module.scss index 99e0284f67..f09ec12bb1 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Twilio/SMS/TwilioSmsConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Twilio/SMS/TwilioSmsConnect.module.scss @@ -2,7 +2,7 @@ @import 'assets/scss/colors.scss'; .wrapper { - background: white; + background: var(--color-background-white); display: block; border-radius: 10px; padding-left: 96px; @@ -15,6 +15,7 @@ .headline { @include font-xl; + color: var(--color-text-contrast); font-weight: bold; margin-bottom: 8px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/SMS/TwilioSmsConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Twilio/SMS/TwilioSmsConnect.tsx similarity index 95% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/SMS/TwilioSmsConnect.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/SMS/TwilioSmsConnect.tsx index 1b815f19e9..ddefedee82 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Twilio/SMS/TwilioSmsConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Twilio/SMS/TwilioSmsConnect.tsx @@ -27,7 +27,7 @@ const TwilioSmsConnect = (props: ConnectedProps) => { }, []); useEffect(() => { - if (channelId !== 'new_account' && channelId?.length) { + if (channelId !== 'new' && channelId?.length) { channels.find((item: Channel) => { return item.id === channelId; }); diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioConnect.module.scss similarity index 93% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioConnect.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioConnect.module.scss index 6fe6bc38a5..6c8af63732 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioConnect.module.scss @@ -23,7 +23,7 @@ width: 300px; border: 1px solid var(--color-light-gray); border-radius: 8px; - background-color: white; + background-color: var(--color-background-white); margin-top: 24px; } @@ -82,7 +82,7 @@ } .wrapper { - background: white; + background: var(--color-background-white); display: block; border-radius: 10px; padding-left: 96px; @@ -95,6 +95,7 @@ .headline { @include font-xl; + color: var(--color-text-contrast); font-weight: bold; margin-bottom: 8px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioConnect.tsx similarity index 91% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioConnect.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioConnect.tsx index bbff624ed2..7e098eeb70 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioConnect.tsx @@ -9,7 +9,7 @@ import {ReactComponent as ArrowLeft} from 'assets/images/icons/arrowLeft.svg'; import styles from './TwilioConnect.module.scss'; -import {CHANNELS_CONNECTED_ROUTE} from '../../../../routes/routes'; +import {CONNECTORS_CONNECTED_ROUTE, CATALOG_CONNECTED_ROUTE} from '../../../../routes/routes'; import {useNavigate} from 'react-router-dom'; type TwilioConnectProps = { @@ -32,6 +32,10 @@ const TwilioConnect = (props: TwilioConnectProps) => { const [nameInput, setNameInput] = useState(channel?.metadata?.name || ''); const [imageUrlInput, setImageUrlInput] = useState(channel?.metadata?.imageUrl || ''); + const CONNECTED_ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_CONNECTED_ROUTE + : CATALOG_CONNECTED_ROUTE; + const handleNumberInput = (e: React.ChangeEvent): void => { setNumberInput(e.target.value); }; @@ -55,7 +59,7 @@ const TwilioConnect = (props: TwilioConnectProps) => { if (source === Source.twilioWhatsApp) { connectTwilioWhatsapp(connectPayload).then(() => { - navigate(CHANNELS_CONNECTED_ROUTE + `/twilio.whatsapp/#`, { + navigate(CONNECTED_ROUTE + `/twilio.whatsapp/#`, { replace: true, state: {source: 'twilio.whatsapp'}, }); @@ -63,7 +67,7 @@ const TwilioConnect = (props: TwilioConnectProps) => { } if (source === Source.twilioSMS) { connectTwilioSms(connectPayload).then(() => { - navigate(CHANNELS_CONNECTED_ROUTE + `/twilio.sms/#`, {replace: true, state: {source: 'twilio.sms'}}); + navigate(CONNECTED_ROUTE + `/twilio.sms/#`, {replace: true, state: {source: 'twilio.sms'}}); }); } }; diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioRequirementsDialog/index.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioRequirementsDialog/index.module.scss similarity index 93% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioRequirementsDialog/index.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioRequirementsDialog/index.module.scss index 7dbcfdd263..75ffbc81a0 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioRequirementsDialog/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioRequirementsDialog/index.module.scss @@ -7,7 +7,7 @@ flex-direction: column; width: 582px; height: 360px; - background: white; + background: var(--color-background-white); border-radius: 8px; z-index: $popup; position: fixed; @@ -24,6 +24,7 @@ .headline { @include font-xl; + color: var(--color-text-contrast); font-weight: bold; padding-top: 40px; padding-left: 40px; diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioRequirementsDialog/index.tsx b/frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioRequirementsDialog/index.tsx similarity index 100% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/TwilioRequirementsDialog/index.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/TwilioRequirementsDialog/index.tsx diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.module.scss b/frontend/control-center/src/pages/Connectors/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.module.scss similarity index 87% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.module.scss rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.module.scss index 99e0284f67..f09ec12bb1 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.module.scss +++ b/frontend/control-center/src/pages/Connectors/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.module.scss @@ -2,7 +2,7 @@ @import 'assets/scss/colors.scss'; .wrapper { - background: white; + background: var(--color-background-white); display: block; border-radius: 10px; padding-left: 96px; @@ -15,6 +15,7 @@ .headline { @include font-xl; + color: var(--color-text-contrast); font-weight: bold; margin-bottom: 8px; } diff --git a/frontend/control-center/src/pages/Channels/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.tsx b/frontend/control-center/src/pages/Connectors/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.tsx similarity index 97% rename from frontend/control-center/src/pages/Channels/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.tsx rename to frontend/control-center/src/pages/Connectors/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.tsx index 6c65378f4a..feb4e558aa 100644 --- a/frontend/control-center/src/pages/Channels/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.tsx +++ b/frontend/control-center/src/pages/Connectors/Providers/Twilio/WhatsApp/TwilioWhatsappConnect.tsx @@ -27,7 +27,7 @@ const TwilioWhatsappConnect = (props: ConnectedProps) => { }, []); useEffect(() => { - if (channelId !== 'new_account') { + if (channelId !== 'new') { channels.find((item: Channel) => { return item.id === channelId; }); diff --git a/frontend/control-center/src/pages/Channels/index.module.scss b/frontend/control-center/src/pages/Connectors/index.module.scss similarity index 92% rename from frontend/control-center/src/pages/Channels/index.module.scss rename to frontend/control-center/src/pages/Connectors/index.module.scss index 9a5c68703d..fd6b21b579 100644 --- a/frontend/control-center/src/pages/Channels/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/index.module.scss @@ -2,7 +2,7 @@ @import 'assets/scss/colors.scss'; .channelsWrapper { - background: white; + background: var(--color-background-white); border-radius: 10px; padding: 32px; margin: 88px 1.5em 0 191px; diff --git a/frontend/control-center/src/pages/Connectors/index.tsx b/frontend/control-center/src/pages/Connectors/index.tsx new file mode 100644 index 0000000000..d29e7e6018 --- /dev/null +++ b/frontend/control-center/src/pages/Connectors/index.tsx @@ -0,0 +1,86 @@ +import React, {useEffect, useState} from 'react'; +import {connect, ConnectedProps, useSelector} from 'react-redux'; +import {useNavigate} from 'react-router-dom'; +import {Channel, Source} from 'model'; +import InfoCard, {InfoCardStyle} from './InfoCard'; +import {StateModel} from '../../reducers'; +import {allChannelsConnected} from '../../selectors/channels'; +import {listChannels} from '../../actions/channel'; +import {setPageTitle} from '../../services/pageTitle'; +import {getSourcesInfo, SourceInfo} from '../../components/SourceInfo'; +import styles from './index.module.scss'; +import {EmptyStateConnectors} from './EmptyStateConnectors'; +import {ChannelCard} from './ChannelCard'; + +const mapDispatchToProps = { + listChannels, +}; + +const mapStateToProps = (state: StateModel) => ({ + channels: Object.values(allChannelsConnected(state)), +}); + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const Connectors = (props: ConnectedProps) => { + const channels = useSelector((state: StateModel) => Object.values(allChannelsConnected(state))); + const channelsBySource = (Source: Source) => channels.filter((channel: Channel) => channel.source === Source); + const [sourcesInfo, setSourcesInfo] = useState([]); + const navigate = useNavigate(); + const pageTitle = 'Connectors'; + + useEffect(() => { + setSourcesInfo(getSourcesInfo(pageTitle)); + }, []); + + useEffect(() => { + if (props.channels.length === 0) { + props.listChannels(); + } + setPageTitle(pageTitle); + }, [props.channels.length]); + + return ( +
+ {sourcesInfo.length > 0 && ( +
+
+

Connectors

+
+
+ )} +
+ {sourcesInfo.length === 0 ? ( + + ) : ( + <> + {sourcesInfo.map( + (infoItem: SourceInfo, index: number) => + (channelsBySource(infoItem.type).length > 0 && infoItem.channel && ( + + )) || + (channelsBySource(infoItem.type).length > 0 && !infoItem.channel && ( +
+ { + navigate(infoItem.channelsListRoute); + }} + /> +
+ )) + )} + + )} +
+
+ ); +}; + +export default connector(Connectors); diff --git a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/ChannelsListItem/index.module.scss b/frontend/control-center/src/pages/Inbox/ChannelsList/ChannelListItem/index.module.scss similarity index 96% rename from frontend/control-center/src/pages/Channels/ConnectedChannelsList/ChannelsListItem/index.module.scss rename to frontend/control-center/src/pages/Inbox/ChannelsList/ChannelListItem/index.module.scss index 3ed9b92b8a..2542db3085 100644 --- a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/ChannelsListItem/index.module.scss +++ b/frontend/control-center/src/pages/Inbox/ChannelsList/ChannelListItem/index.module.scss @@ -46,6 +46,7 @@ align-items: center; padding-left: 16px; padding-right: 16px; + color: var(--color-text-gray); &:empty { padding-right: 0px; } @@ -91,6 +92,12 @@ height: 50px; position: absolute; right: 44px; + svg { + color: var(--color-text-gray); + &:hover { + color: var(--color-airy-blue); + } + } } .disabledDisconnect { diff --git a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/ChannelsListItem/index.tsx b/frontend/control-center/src/pages/Inbox/ChannelsList/ChannelListItem/index.tsx similarity index 78% rename from frontend/control-center/src/pages/Channels/ConnectedChannelsList/ChannelsListItem/index.tsx rename to frontend/control-center/src/pages/Inbox/ChannelsList/ChannelListItem/index.tsx index cdad9d685f..8a9edd3ca4 100644 --- a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/ChannelsListItem/index.tsx +++ b/frontend/control-center/src/pages/Inbox/ChannelsList/ChannelListItem/index.tsx @@ -6,8 +6,9 @@ import {disconnectChannel} from '../../../../actions/channel'; import {SettingsModal, Button} from 'components'; import {Channel} from 'model'; -import {ReactComponent as CheckMarkIcon} from 'assets/images/icons/checkmark.svg'; -import ChannelAvatar from '../../../../components/ChannelAvatar'; +import {ReactComponent as CheckMarkFilledIcon} from 'assets/images/icons/checkmarkFilled.svg'; +import {ReactComponent as PencilIcon} from 'assets/images/icons/pencil.svg'; +import {ReactComponent as DisconnectIcon} from 'assets/images/icons/disconnectIcon.svg'; import styles from './index.module.scss'; import {useNavigate} from 'react-router-dom'; @@ -26,6 +27,7 @@ const ChannelListItem = (props: ChannelListItemProps) => { const {channel} = props; const navigate = useNavigate(); const [deletePopupVisible, setDeletePopupVisible] = useState(false); + const path = location.pathname.includes('connectors') ? 'connectors' : 'catalog'; const togglePopupVisibility = () => { setDeletePopupVisible(!deletePopupVisible); @@ -47,31 +49,24 @@ const ChannelListItem = (props: ChannelListItemProps) => { <>
-
- -
+ {channel.connected && }
{channel.metadata?.name}
{isPhoneNumberSource() &&
{channel.sourceChannelId}
} - {channel.connected && ( -
- Connected -
- )} -
-
diff --git a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/index.module.scss b/frontend/control-center/src/pages/Inbox/ChannelsList/index.module.scss similarity index 79% rename from frontend/control-center/src/pages/Channels/ConnectedChannelsList/index.module.scss rename to frontend/control-center/src/pages/Inbox/ChannelsList/index.module.scss index fc52dd9f36..6e31066542 100644 --- a/frontend/control-center/src/pages/Channels/ConnectedChannelsList/index.module.scss +++ b/frontend/control-center/src/pages/Inbox/ChannelsList/index.module.scss @@ -13,10 +13,28 @@ min-height: calc(100vh - 170px); } +.headlineRow { + display: flex; + place-content: space-between; + margin-top: 32px; + margin-bottom: 16px; +} + .headline { @include font-xl; font-weight: bold; - margin-bottom: 8px; +} + +.description { + @include font-base; + color: var(--color-text-gray); +} + +.linkButtonContainer { + @include font-base; + display: flex; + align-items: center; + color: var(--color-text-contrast); } .backButton { @@ -25,17 +43,16 @@ cursor: pointer; text-decoration: none; max-width: 200px; - color: var(--color-airy-blue); &:hover { text-decoration: underline; } } .backIcon { - height: 13px; - width: 17px; + height: 20px; + width: 20px; path { - fill: var(--color-airy-blue); + fill: var(--color-text-contrast); } margin-right: 8px; } @@ -95,11 +112,6 @@ color: var(--color-soft-green); } -.headlineRow { - display: flex; - place-content: space-between; -} - .buttons { display: flex; flex-direction: row; @@ -107,12 +119,12 @@ button { align-self: center; - height: 40px; - min-width: 40px; + height: 32px; + width: 32px; margin-left: 16px; + border-radius: 32px; background: var(--color-blue-white); border: none; - border-radius: 4px; cursor: pointer; } @@ -121,6 +133,10 @@ } button:last-child { + border-radius: 48px; + height: 32px; + width: 32px; + border-radius: 32px; background: var(--color-airy-blue); svg { path { @@ -140,19 +156,20 @@ .searchField { min-width: 300px; - animation-name: searchFieldAnimation; - animation-duration: 500ms; - animation-fill-mode: forwards; + animation: searchFieldAnimation 3000ms ease; + height: 32px; + border-radius: 32px; } @keyframes searchFieldAnimation { 0% { - margin-left: 0px; - width: 0%; + width: 0px; + opacity: 0; } + 100% { - margin-left: 16px; width: 200px; + opacity: 1; } } @@ -166,6 +183,7 @@ .closeIcon { height: 10px; width: 10px; + z-index: 2; } .plusIcon { @@ -176,4 +194,5 @@ .searchIcon { height: 18px; width: 18px; + color: var(--color-text-gray); } diff --git a/frontend/control-center/src/pages/Inbox/ChannelsList/index.tsx b/frontend/control-center/src/pages/Inbox/ChannelsList/index.tsx new file mode 100644 index 0000000000..048ea1a2a1 --- /dev/null +++ b/frontend/control-center/src/pages/Inbox/ChannelsList/index.tsx @@ -0,0 +1,195 @@ +import React, {useLayoutEffect, useState} from 'react'; +import {useSelector} from 'react-redux'; +import {useNavigate, useParams} from 'react-router-dom'; +import {sortBy} from 'lodash-es'; + +import {StateModel} from '../../../reducers'; +import {allChannels} from '../../../selectors/channels'; + +import {Channel, Source} from 'model'; +import ChannelsListItem from './ChannelListItem'; +import {SearchField, LinkButton, Button} from 'components'; +import {ReactComponent as ArrowLeftIcon} from 'assets/images/icons/leftArrowCircle.svg'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {ReactComponent as PlusIcon} from 'assets/images/icons/plus.svg'; +import {ReactComponent as CloseIcon} from 'assets/images/icons/close.svg'; + +import styles from './index.module.scss'; +import {cyChannelsFormBackButton} from 'handles'; +import { + CONNECTORS_FACEBOOK_ROUTE, + CONNECTORS_CHAT_PLUGIN_ROUTE, + CONNECTORS_TWILIO_SMS_ROUTE, + CONNECTORS_TWILIO_WHATSAPP_ROUTE, + CONNECTORS_GOOGLE_ROUTE, + CONNECTORS_INSTAGRAM_ROUTE, + CATALOG_FACEBOOK_ROUTE, + CATALOG_CHAT_PLUGIN_ROUTE, + CATALOG_TWILIO_SMS_ROUTE, + CATALOG_TWILIO_WHATSAPP_ROUTE, + CATALOG_GOOGLE_ROUTE, + CATALOG_INSTAGRAM_ROUTE, +} from '../../../routes/routes'; +import {getChannelAvatar} from '../../../components/ChannelAvatar'; + +const ChannelsList = () => { + const {source} = useParams(); + const navigate = useNavigate(); + const channels = useSelector((state: StateModel) => { + return Object.values(allChannels(state)).filter((channel: Channel) => channel.source === source); + }); + + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [path, setPath] = useState(''); + const [searchText, setSearchText] = useState(''); + const [showingSearchField, setShowingSearchField] = useState(false); + + const filteredChannels = channels.filter((channel: Channel) => + channel.metadata?.name?.toLowerCase().includes(searchText.toLowerCase()) + ); + + useLayoutEffect(() => { + getInfo(); + }, [source, channels]); + + const getInfo = () => { + let ROUTE; + switch (source) { + case Source.facebook: + setName('Facebook Messenger'); + setDescription('Best of class browser messenger'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_FACEBOOK_ROUTE : CATALOG_FACEBOOK_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.google: + setName('Google Business Messages'); + setDescription('Best of class browser messenger'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_GOOGLE_ROUTE : CATALOG_GOOGLE_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.twilioSMS: + setName('Twilio SMS'); + setDescription('Best of class browser messenger'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_TWILIO_SMS_ROUTE : CATALOG_TWILIO_SMS_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.twilioWhatsApp: + setName('Twilio Whatsapp'); + setDescription('Best of class browser messenger'); + ROUTE = location.pathname.includes('connectors') + ? CONNECTORS_TWILIO_WHATSAPP_ROUTE + : CATALOG_TWILIO_WHATSAPP_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.chatPlugin: + setName('Chat Plugin'); + setDescription('Best of class browser messenger'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_CHAT_PLUGIN_ROUTE : CATALOG_CHAT_PLUGIN_ROUTE; + setPath(ROUTE + '/new'); + break; + case Source.instagram: + setName('Instagram'); + setDescription('Best of class browser messenger'); + ROUTE = location.pathname.includes('connectors') ? CONNECTORS_INSTAGRAM_ROUTE : CATALOG_INSTAGRAM_ROUTE; + setPath(ROUTE + '/new'); + break; + } + }; + + const showSearchFieldToggle = () => { + setShowingSearchField(!showingSearchField); + setSearchText(''); + }; + + return ( +
+ navigate(-1)} type="button"> +
+ + Channels +
+
+
+
+
{getChannelAvatar(source)}
+
+

{name}

+

{description}

+
+
+ +
+
+
+
+ {showingSearchField && ( + setSearchText(value)} + autoFocus={true} + style={{height: '32px', borderRadius: '32px'}} + resetClicked={() => setSearchText('')} + /> + )} +
+
+
+ + +
+
+
+ Name + Manage +
+ +
+ {filteredChannels.length > 0 ? ( + sortBy(filteredChannels, (channel: Channel) => channel.metadata.name.toLowerCase()).map( + (channel: Channel) => ( +
+ +
+ ) + ) + ) : ( +
+

Result not found.

+

Try to search for a different term.

+
+ )} +
+
+ ); +}; + +export default ChannelsList; diff --git a/frontend/control-center/src/pages/Inbox/EmptyStateInbox/index.module.scss b/frontend/control-center/src/pages/Inbox/EmptyStateInbox/index.module.scss new file mode 100644 index 0000000000..b9b02ae3ac --- /dev/null +++ b/frontend/control-center/src/pages/Inbox/EmptyStateInbox/index.module.scss @@ -0,0 +1,43 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.container { + display: flex; + flex-direction: column; + flex: 1; + align-items: center; + justify-content: center; + margin-top: 10%; + + h1 { + @include font-xl; + font-weight: 800; + margin-top: 32px; + margin-bottom: 32px; + } + + p { + @include font-m; + text-align: center; + } +} + +.searchIconContainer { + display: flex; + justify-content: center; + align-items: center; + height: 105px; + width: 105px; + background: var(--color-background-blue); +} + +.linkContainer { + @include font-m; + display: flex; + align-items: center; + color: var(--color-airy-blue); + text-decoration: none; + margin-right: 4px; + width: 95px; + justify-content: space-around; +} diff --git a/frontend/control-center/src/pages/Inbox/EmptyStateInbox/index.tsx b/frontend/control-center/src/pages/Inbox/EmptyStateInbox/index.tsx new file mode 100644 index 0000000000..567278201d --- /dev/null +++ b/frontend/control-center/src/pages/Inbox/EmptyStateInbox/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {ReactComponent as CatalogIcon} from 'assets/images/icons/catalogIcon.svg'; +import {Link} from 'react-router-dom'; +import {CATALOG_ROUTE} from '../../../routes/routes'; + +export const EmptyStateInbox = () => { + return ( +
+
+ +
+

No Sources installed

+

You don't have any conversational sources installed, please open the

+
+ + + Catalog + +

and explore more.

+
+
+ ); +}; diff --git a/frontend/control-center/src/pages/Inbox/InboxOutlet.tsx b/frontend/control-center/src/pages/Inbox/InboxOutlet.tsx new file mode 100644 index 0000000000..80d96e46f5 --- /dev/null +++ b/frontend/control-center/src/pages/Inbox/InboxOutlet.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import {Outlet} from 'react-router-dom'; + +const InboxOutlet = () => { + return ; +}; + +export default InboxOutlet; diff --git a/frontend/control-center/src/pages/Inbox/index.module.scss b/frontend/control-center/src/pages/Inbox/index.module.scss new file mode 100644 index 0000000000..194b988a0c --- /dev/null +++ b/frontend/control-center/src/pages/Inbox/index.module.scss @@ -0,0 +1,48 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.inboxWrapper { + background: white; + border-radius: 10px; + padding: 32px; + margin: 88px 1.5em 0 191px; + height: calc(100vh - 88px); + overflow-y: scroll; + overflow-x: hidden; + width: 100%; +} + +.inboxHeadline { + @include font-xl; + font-weight: 900; + letter-spacing: 0; + display: flex; + justify-content: space-between; + color: var(--color-text-contrast); + margin-bottom: 14px; +} + +.inboxHeadlineText { + @include font-xl; + font-weight: 900; +} + +.wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.channelsContainer { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.channelsLine { + width: 100%; + span { + @include font-base; + color: var(--color-airy-blue); + } +} diff --git a/frontend/control-center/src/pages/Inbox/index.tsx b/frontend/control-center/src/pages/Inbox/index.tsx new file mode 100644 index 0000000000..0a36626868 --- /dev/null +++ b/frontend/control-center/src/pages/Inbox/index.tsx @@ -0,0 +1,78 @@ +import {Source} from 'model'; +import {Channel} from 'model/Channel'; +import React, {useEffect, useState} from 'react'; +import {connect, ConnectedProps, useSelector} from 'react-redux'; +import {listChannels} from '../../actions/channel'; +import {getSourcesInfo, SourceInfo} from '../../components/SourceInfo'; +import {StateModel} from '../../reducers'; +import {allChannelsConnected} from '../../selectors/channels'; +import {setPageTitle} from '../../services/pageTitle'; +import {ChannelCard} from '../Connectors/ChannelCard'; +import {EmptyStateInbox} from './EmptyStateInbox'; +import styles from './index.module.scss'; + +const mapDispatchToProps = { + listChannels, +}; + +const mapStateToProps = (state: StateModel) => ({ + channels: Object.values(allChannelsConnected(state)), +}); + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const Inbox = (props: ConnectedProps) => { + const [sourcesInfo, setSourcesInfo] = useState([]); + const channels = useSelector((state: StateModel) => Object.values(allChannelsConnected(state))); + const channelsBySource = (Source: Source) => channels.filter((channel: Channel) => channel.source === Source); + + useEffect(() => { + setSourcesInfo(getSourcesInfo('Inbox')); + }, []); + + useEffect(() => { + if (props.channels.length === 0) { + props.listChannels(); + } + }, [props.channels.length]); + + useEffect(() => { + setPageTitle('Inbox'); + }, []); + + return ( +
+ {sourcesInfo.length > 0 && ( +
+
+

Inbox

+
+
+ )} +
+ {sourcesInfo.length === 0 ? ( + + ) : ( +
+
+ Channels +
+
+
+
+
+
+ {sourcesInfo.map((infoItem: SourceInfo, index: number) => { + if (channelsBySource(infoItem.type).length > 0) { + return ; + } + })} +
+
+ )} +
+
+ ); +}; + +export default connector(Inbox); diff --git a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx index 9db2a6fff4..7e1c433c12 100644 --- a/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx +++ b/frontend/control-center/src/pages/Status/ComponentListItem/ItemInfo.tsx @@ -2,7 +2,6 @@ import React, {useState} from 'react'; import {ReactComponent as CheckmarkIcon} from 'assets/images/icons/checkmarkFilled.svg'; import {ReactComponent as UncheckedIcon} from 'assets/images/icons/serviceUnhealthy.svg'; import {ReactComponent as ArrowRight} from 'assets/images/icons/arrowRight.svg'; -import {ReactComponent as ArrowDown} from 'assets/images/icons/arrowDown.svg'; import {getChannelAvatar} from '../../../components/ChannelAvatar'; import {getComponentName} from '../../../services'; import {getSourceForComponent} from 'model'; @@ -14,20 +13,15 @@ type ComponentInfoProps = { itemName: string; isComponent: boolean; isExpanded: boolean; - setIsExpanded: React.Dispatch>; enabled?: boolean; }; export const ItemInfo = (props: ComponentInfoProps) => { - const {healthy, itemName, isComponent, isExpanded, setIsExpanded, enabled} = props; + const {healthy, itemName, isComponent, isExpanded, enabled} = props; const [channelSource] = useState(itemName && getSourceForComponent(itemName)); const [componentName] = useState(itemName && getComponentName(itemName)); const [componentEnabled, setComponentEnabled] = useState(enabled); - const toggleExpanded = () => { - setIsExpanded(!isExpanded); - }; - return (
{ }`} >
- {isComponent && ( + {isComponent ? ( <> - +
+ +
{getChannelAvatar(channelSource)}
+ ) : ( + <> +
+ )}

diff --git a/frontend/control-center/src/pages/Status/ComponentListItem/index.module.scss b/frontend/control-center/src/pages/Status/ComponentListItem/index.module.scss index bc7dc00f3c..53bcf3bf44 100644 --- a/frontend/control-center/src/pages/Status/ComponentListItem/index.module.scss +++ b/frontend/control-center/src/pages/Status/ComponentListItem/index.module.scss @@ -7,15 +7,19 @@ padding-bottom: 20px; display: flex; flex-direction: column; - background: white; + background: var(--color-background-white); border-left: 1px solid var(--color-light-gray); border-right: 1px solid var(--color-light-gray); border-bottom: 1px solid var(--color-light-gray); - transition: all 0.5s ease; + transition: all 0.5s ease-out; + &:hover { + cursor: pointer; + } } .wrapperExpanded { display: inline-block; + z-index: 1; } .container { @@ -51,12 +55,29 @@ border: none; cursor: pointer; } + + svg { + fill: var(--color-text-contrast); + } } .arrowDownIcon { width: 15px; display: flex; align-items: center; + margin: 0px 40px; +} + +.arrowDownIconOpen { + transition: 300ms ease; + margin-bottom: 0; + transform: rotate(90deg); +} + +.arrowDownIconClose { + transition: 300ms ease; + margin-bottom: 0; + transform: rotate(0deg); } .componentName { @@ -66,7 +87,7 @@ } .serviceName { - margin-left: 25%; + margin-left: 10%; @include font-s; color: var(--color-text-gray); } @@ -89,4 +110,9 @@ display: flex; justify-content: center; align-items: center; + z-index: 2; +} + +.blankSpace { + width: 60px; } diff --git a/frontend/control-center/src/pages/Status/ComponentListItem/index.tsx b/frontend/control-center/src/pages/Status/ComponentListItem/index.tsx index 1db3ccf886..07ce71c1b2 100644 --- a/frontend/control-center/src/pages/Status/ComponentListItem/index.tsx +++ b/frontend/control-center/src/pages/Status/ComponentListItem/index.tsx @@ -14,27 +14,30 @@ export const ComponentListItem = (props: ComponentsListProps) => { const [isExpanded, setIsExpanded] = useState(false); const wrapper = useRef(null); const paddingWrapper = 20; + const defaultHeight = 50; + const serviceItemHeight = 24; useEffect(() => { if (wrapper && wrapper.current) { if (isExpanded) { - wrapper.current.style.height = `${wrapper.current.scrollHeight + paddingWrapper}px`; + wrapper.current.style.height = `${defaultHeight + services.length * (serviceItemHeight + paddingWrapper)}px`; } else { - wrapper.current.style.height = '50px'; + wrapper.current.style.height = `${defaultHeight}px`; } } }, [isExpanded]); + const toggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + return ( -

- +
+ {services.map((service, index) => ( { itemName={service.name} isComponent={false} isExpanded={isExpanded} - setIsExpanded={setIsExpanded} key={index} /> ))} diff --git a/frontend/control-center/src/pages/Status/index.module.scss b/frontend/control-center/src/pages/Status/index.module.scss index 5fa53cc460..67f4412637 100644 --- a/frontend/control-center/src/pages/Status/index.module.scss +++ b/frontend/control-center/src/pages/Status/index.module.scss @@ -7,6 +7,7 @@ padding: 28px 18px; margin: 88px 1.5em 0 191px; background: var(--color-blue-white); + color: var(--color-text-contrast); border-radius: 10px; overflow-y: scroll; overflow-x: hidden; @@ -25,8 +26,8 @@ .listHeader { display: flex; flex-direction: row; - background-color: var(--color-airy-blue); - border: 1px solide var(--color-airy-blue); + background-color: var(--color-elements-blue); + border: 1px solide var(--color-elements-blue); border-top-right-radius: 10px; border-top-left-radius: 10px; height: 50px; diff --git a/frontend/control-center/src/pages/Status/index.tsx b/frontend/control-center/src/pages/Status/index.tsx index 01d8b750c0..7c53aab25d 100644 --- a/frontend/control-center/src/pages/Status/index.tsx +++ b/frontend/control-center/src/pages/Status/index.tsx @@ -14,7 +14,7 @@ const mapDispatchToProps = { const connector = connect(null, mapDispatchToProps); const Status = (props: ConnectedProps) => { - const config = useSelector((state: StateModel) => state.data.config); + const components = useSelector((state: StateModel) => Object.entries(state.data.config.components)); const [spinAnim, setSpinAnim] = useState(true); useEffect(() => { @@ -50,15 +50,19 @@ const Status = (props: ConnectedProps) => {
- {Object.entries(config.components).map((component, index) => ( - - ))} + {components + .filter(component => { + return component[1].enabled; + }) + .map((component, index) => ( + + ))}
); diff --git a/frontend/control-center/src/pages/Webhooks/SubscriptionModal/index.module.scss b/frontend/control-center/src/pages/Webhooks/SubscriptionModal/index.module.scss new file mode 100644 index 0000000000..9a55a4b530 --- /dev/null +++ b/frontend/control-center/src/pages/Webhooks/SubscriptionModal/index.module.scss @@ -0,0 +1,127 @@ +@import 'assets/scss/colors.scss'; +@import 'assets/scss/fonts.scss'; + +.formContainer { + display: flex; + flex-direction: column; + width: 100%; + min-height: 580px; + align-items: center; +} + +.container { + @include font-base; + color: var(--color-text-gray); + background: var(--color-background-white); + height: 500px; + + h1 { + margin-top: 45px; + margin-bottom: 28px; + font-weight: 700; + } +} + +.checkboxContainer { + display: flex; + align-items: center; + span { + @include font-base; + cursor: pointer; + margin-right: 96px; + } + input { + @include font-base; + cursor: pointer; + height: 16px; + width: 16px; + background-color: var(--color-background-white); + } +} + +.inputContainer { + display: flex; + + input { + @include font-base; + border: none; + background: var(--color-background-gray); + color: var(--color-text-contrast); + padding: 6px; + border-radius: 8px; + margin-right: 32px; + height: 50px; + padding: 16px 21px; + } + + input:focus { + outline: none; + border: 1px solid var(--color-airy-blue); + } + + input:last-child { + @include font-base; + width: 400px; + } +} + +.headerKeyContainer { + display: flex; + flex-direction: row; + margin-top: 48px; + margin-bottom: 48px; + + span { + @include font-base; + color: var(--color-text-contrast); + margin-bottom: 28px; + font-weight: 800; + } + + input { + @include font-base; + width: 408px; + background: var(--color-background-gray); + border-radius: 6px; + color: var(--color-red-info); + padding: 17px 12px; + border: none; + } + + input:focus { + outline: none; + border: 1px solid var(--color-airy-blue); + } +} + +.headerKeyItem { + display: flex; + flex-direction: column; + margin-right: 42px; +} + +.errorMessage { + @include font-base; + display: flex; + position: absolute; + bottom: 24px; + color: var(--color-red-alert); + font-size: 16px; +} + +@keyframes spinAnimationRefresh { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(7200deg); + } +} + +.spinAnimation { + svg { + height: 20px; + width: 20px; + animation: spinAnimationRefresh 40s linear; + } +} diff --git a/frontend/control-center/src/pages/Webhooks/SubscriptionModal/index.tsx b/frontend/control-center/src/pages/Webhooks/SubscriptionModal/index.tsx new file mode 100644 index 0000000000..4648a1bd2a --- /dev/null +++ b/frontend/control-center/src/pages/Webhooks/SubscriptionModal/index.tsx @@ -0,0 +1,217 @@ +import {Button} from 'components/cta/Button'; +import React, {useLayoutEffect, useState} from 'react'; +import {ReactComponent as RefreshIcon} from 'assets/images/icons/refreshIcon.svg'; +import styles from './index.module.scss'; +import {Webhook, WebhooksEventType} from 'model/Webhook'; + +type SubscriptionModalProps = { + webhook: Webhook; + messageCreated?: boolean; + messageUpdated?: boolean; + conversationUpdated?: boolean; + channelUpdated?: boolean; + newWebhook?: boolean; + isLoading?: boolean; + error?: boolean; + setUpsertWebhook?: (isNew: boolean, webhook: Webhook) => void; +}; + +const isEventOn = (events: WebhooksEventType[] | undefined, event: WebhooksEventType): boolean => { + return events?.includes(event); +}; + +const SubscriptionModal = (props: SubscriptionModalProps) => { + const {webhook, newWebhook, isLoading, error, setUpsertWebhook} = props; + const {name, url, events, headers, signatureKey} = webhook; + const [buttonTitle, setButtonTitle] = useState(''); + const [newUrl, setNewUrl] = useState(newWebhook ? '' : url); + const [newName, setNewName] = useState(name || ''); + const [newEvents, setNewEvents] = useState(events || []); + const [newHeaders, setNewHeaders] = useState(headers['X-Custom-Header'] || ''); + const [newSignatureKey, setNewSignatureKey] = useState(signatureKey || ''); + const [messageCreatedChecked, setMessageCreatedChecked] = useState( + isEventOn(events, WebhooksEventType.messageCreated) + ); + const [messageUpdatedChecked, setMessageUpdatedChecked] = useState( + isEventOn(events, WebhooksEventType.messageUpdated) + ); + const [conversationUpdatedChecked, setConversationUpdatedChecked] = useState( + isEventOn(events, WebhooksEventType.conversationUpdated) + ); + const [channelUpdatedChecked, setChannelUpdatedChecked] = useState( + isEventOn(events, WebhooksEventType.channelUpdated) + ); + + const handleChecked = (event: WebhooksEventType) => { + switch (event) { + case WebhooksEventType.messageCreated: { + setMessageCreatedChecked(!messageCreatedChecked); + !newEvents.includes(event) + ? setNewEvents([...newEvents, WebhooksEventType.messageCreated]) + : setNewEvents(newEvents.filter(item => item !== event)); + break; + } + case WebhooksEventType.messageUpdated: { + setMessageUpdatedChecked(!messageUpdatedChecked); + !newEvents.includes(event) + ? setNewEvents([...newEvents, WebhooksEventType.messageUpdated]) + : setNewEvents(newEvents.filter(item => item !== event)); + break; + } + case WebhooksEventType.conversationUpdated: { + setConversationUpdatedChecked(!conversationUpdatedChecked); + !newEvents.includes(event) + ? setNewEvents([...newEvents, WebhooksEventType.conversationUpdated]) + : setNewEvents(newEvents.filter(item => item !== event)); + break; + } + case WebhooksEventType.channelUpdated: { + setChannelUpdatedChecked(!channelUpdatedChecked); + !newEvents.includes(event) + ? setNewEvents([...newEvents, WebhooksEventType.channelUpdated]) + : setNewEvents(newEvents.filter(item => item !== event)); + break; + } + } + }; + + useLayoutEffect(() => { + getButtonTitle(); + }, [isLoading, error]); + + const getButtonTitle = () => { + if (error) { + return setButtonTitle('Try again...'); + } + if (!isLoading) { + if (newWebhook) { + return setButtonTitle('Subscribe'); + } else { + return setButtonTitle('Update'); + } + } else { + if (newWebhook) { + return setButtonTitle('Subscribing...'); + } else { + return setButtonTitle('Updating...'); + } + } + }; + + const upsertWebhook = (isNew: boolean) => { + setUpsertWebhook(isNew, { + ...webhook, + ...(newUrl && { + url: newUrl, + }), + ...(newName && { + name: newName, + }), + ...(newEvents && { + events: newEvents, + }), + ...(newSignatureKey && { + signatureKey: newSignatureKey, + }), + ...(newSignatureKey && { + signatureKey: newSignatureKey, + }), + ...(newHeaders && { + headers: {'X-Custom-Header': newHeaders}, + }), + }); + }; + + return ( +
+
+

WEBHOOK

+
+ setNewName(event.target.value)} /> + setNewUrl(event.target.value)} + autoFocus={newWebhook ? true : false} + required={true} + /> +
+

ALL EVENTS

+
+ handleChecked(WebhooksEventType.messageCreated)} + /> + + handleChecked(WebhooksEventType.messageUpdated)} + /> + + handleChecked(WebhooksEventType.conversationUpdated)} + /> + + handleChecked(WebhooksEventType.channelUpdated)} + /> + +
+
+
+ (Customer Header)* + setNewHeaders(event.target.value)}> +
+
+ *Sign key + setNewSignatureKey(event.target.value)} /> +
+
+
+
+ +
+ {error && Unable to {newWebhook ? 'subscribe' : 'update'} Webhook} +
+ ); +}; + +export default SubscriptionModal; diff --git a/frontend/control-center/src/pages/Webhooks/UnsubscribeModal/index.module.scss b/frontend/control-center/src/pages/Webhooks/UnsubscribeModal/index.module.scss new file mode 100644 index 0000000000..47935ef7be --- /dev/null +++ b/frontend/control-center/src/pages/Webhooks/UnsubscribeModal/index.module.scss @@ -0,0 +1,64 @@ +@import 'assets/scss/colors.scss'; +@import 'assets/scss/fonts.scss'; + +.container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + min-height: 520px; + min-width: 1000px; + color: var(--color-text-gray); + background: var(--color-background-white); + + h1 { + @include font-xl; + color: var(--color-text-contrast); + font-weight: 800; + margin-top: 50px; + margin-bottom: 16px; + } + p { + @include font-m; + color: var(--color-text-contrast); + font-weight: 400; + margin-bottom: 50px; + max-width: 75%; + line-break: anywhere; + text-align: center; + } +} + +.buttonContainer { + display: flex; + align-items: center; + button { + @include font-base; + margin: 0 12px; + } +} + +.errorMessage { + @include font-base; + position: absolute; + bottom: 40px; + color: var(--color-red-alert); + font-size: 16px; +} + +@keyframes spinAnimationRefresh { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(7200deg); + } +} + +.spinAnimation { + svg { + height: 20px; + width: 20px; + animation: spinAnimationRefresh 40s linear; + } +} diff --git a/frontend/control-center/src/pages/Webhooks/UnsubscribeModal/index.tsx b/frontend/control-center/src/pages/Webhooks/UnsubscribeModal/index.tsx new file mode 100644 index 0000000000..80998f8f39 --- /dev/null +++ b/frontend/control-center/src/pages/Webhooks/UnsubscribeModal/index.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import styles from './index.module.scss'; + +import {ReactComponent as ErrorMessage} from 'assets/images/icons/errorMessage.svg'; +import {ReactComponent as RefreshIcon} from 'assets/images/icons/refreshIcon.svg'; +import {Button} from 'components/cta/Button'; + +type UnsubscribeModalProps = { + setUnsubscribe: (unsubscribe: boolean) => void; + setCancelUnsubscribe: (cancel: boolean) => void; + webhookUrl: string; + isLoading: boolean; + error: boolean; +}; + +export const UnsubscribeModal = (props: UnsubscribeModalProps) => { + const {setUnsubscribe, setCancelUnsubscribe, webhookUrl, isLoading, error} = props; + + const handleConfirm = () => { + setUnsubscribe(true); + }; + + const handleCancel = () => { + setCancelUnsubscribe(true); + }; + + return ( +
+ +

Unsubscribe Webhook

+

+ Are you sure
you want to unsubscribe

{webhookUrl}? +

+
+
+ + +
+
+ {error && Unable to unsubscribe Webhook} +
+ ); +}; diff --git a/frontend/control-center/src/pages/Webhooks/WebhooksListItem/index.module.scss b/frontend/control-center/src/pages/Webhooks/WebhooksListItem/index.module.scss new file mode 100644 index 0000000000..a23a8b5ae3 --- /dev/null +++ b/frontend/control-center/src/pages/Webhooks/WebhooksListItem/index.module.scss @@ -0,0 +1,140 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.container { + @include font-base; + display: flex; + align-items: flex-start; + background: var(--color-background-white); + height: 'auto'; + border-bottom: 1px solid var(--color-light-gray); + margin-top: 30px; + padding-bottom: 30px; + + span { + width: 40%; + color: var(--color-text-contrast); + text-decoration: underline; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 8px; + } + + p { + width: 25%; + color: var(--color-text-gray); + overflow: hidden; + text-overflow: ellipsis; + padding-right: 8px; + white-space: nowrap; + } + + div:not(:last-child) { + margin-bottom: 3px; + } + + input:first-child { + width: 40%; + } + + input:not(:first-child, :last-child) { + width: 25%; + } + input { + width: 25%; + } +} + +.eventsContainer { + display: flex; + flex-direction: column; + width: 25%; + p { + @include font-base; + margin-bottom: 22px; + } + p:last-child { + margin-bottom: 0px; + } + input { + @include font-base; + margin-bottom: 22px; + } +} + +.statusContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + + svg path { + fill: var(--color-text-gray); + } +} + +.pensilIcon { + cursor: pointer; +} + +.inputs { + width: 100%; + height: 24px; + border: none; + font-size: 16px; + padding: 0px; +} + +.displayNameButtons { + display: flex; + + button { + border: none; + outline: none; + border-radius: 50%; + height: 24px; + width: 24px; + cursor: pointer; + background-color: transparent; + } + + .cancelEdit { + border: 1px solid var(--color-text-gray); + margin-left: 8px; + svg { + path { + fill: var(--color-text-gray); + } + } + &:hover { + border: 1px solid var(--color-red-alert); + svg { + path { + fill: var(--color-red-alert); + } + } + } + } + + .saveEdit { + border: 1px solid var(--color-soft-green); + margin-left: 8px; + svg { + height: 8px; + margin-bottom: 0.5px; + } + } + + .disabledSaveEdit { + border: 1px solid var(--color-text-gray); + margin-left: 8px; + svg { + height: 8px; + margin-bottom: 0.5px; + path { + fill: var(--color-text-gray); + } + } + } +} diff --git a/frontend/control-center/src/pages/Webhooks/WebhooksListItem/index.tsx b/frontend/control-center/src/pages/Webhooks/WebhooksListItem/index.tsx new file mode 100644 index 0000000000..e7c30ecb3e --- /dev/null +++ b/frontend/control-center/src/pages/Webhooks/WebhooksListItem/index.tsx @@ -0,0 +1,166 @@ +import React, {useState} from 'react'; +import {ReactComponent as PensilIcon} from 'assets/images/icons/pencil.svg'; +import styles from './index.module.scss'; +import {Switch} from '../../../components/Switch'; +import {SettingsModal} from 'components'; +import SubscriptionModal from '../SubscriptionModal'; +import {UnsubscribeModal} from '../UnsubscribeModal'; +import {Webhook, WebhooksStatus} from 'model/Webhook'; + +type WebhooksListItemProps = { + webhook: Webhook; + switchId?: string; + upsertWebhook: ( + isNew: boolean, + webhook: Webhook, + onCall?: () => void, + onResponse?: () => void, + onError?: (error: Error) => void + ) => void; + setShowNotification?: (show: boolean, error?: boolean) => void; +}; + +const WebhooksListItem = (props: WebhooksListItemProps) => { + const {webhook, switchId, upsertWebhook} = props; + const {name, url, events, status} = webhook; + const [subscribed, setSubscribed] = useState(status || WebhooksStatus.subscribed); + const [editModeOn, setEditModeOn] = useState(false); + const [showUnsubscribeModal, setShowUnsubscribeModal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [errorOccurred, setErrorOccurred] = useState(false); + + const cancelChanges = () => { + setEditModeOn(false); + setShowUnsubscribeModal(false); + }; + + const handleSubscribeToggle = () => { + subscribed === WebhooksStatus.subscribed + ? setShowUnsubscribeModal(true) + : upsertWebhook( + false, + { + ...webhook, + status: WebhooksStatus.subscribed, + }, + () => { + props.setShowNotification(false); + }, + () => { + setSubscribed(WebhooksStatus.subscribed); + props.setShowNotification(true); + setTimeout(() => { + props.setShowNotification(false); + }, 4000); + }, + (error: Error) => { + console.error(error); + props.setShowNotification(true, true); + setTimeout(() => { + props.setShowNotification(false); + }, 4000); + } + ); + }; + + const editWebhook = () => { + setEditModeOn(!editModeOn); + }; + + const upsertWebhookConfirm = (isNew: boolean, webhook: Webhook) => { + upsertWebhook( + isNew, + webhook, + () => { + setErrorOccurred(false); + setIsLoading(true); + }, + () => { + setIsLoading(false); + setEditModeOn(false); + }, + (error: Error) => { + console.error(error); + setErrorOccurred(true); + setIsLoading(false); + } + ); + }; + + const unsubscribeWebhookConfirm = () => { + upsertWebhook( + false, + { + ...webhook, + status: WebhooksStatus.unsubscribed, + }, + () => { + setErrorOccurred(false); + setIsLoading(true); + }, + () => { + setShowUnsubscribeModal(false); + setSubscribed(WebhooksStatus.unsubscribed); + setIsLoading(false); + }, + (error: Error) => { + console.error(error); + setErrorOccurred(true); + setIsLoading(false); + } + ); + }; + + return ( +
+ {url} +

{name}

+
+ <> + {events && + events.map((event, index) => ( +

+ {event} +

+ ))} + +
+
+ +
+ +
+
+ + {editModeOn && ( + + + + )} + {showUnsubscribeModal && ( + + + + )} +
+ ); +}; + +export default WebhooksListItem; diff --git a/frontend/control-center/src/pages/Webhooks/index.module.scss b/frontend/control-center/src/pages/Webhooks/index.module.scss index 19084c95bf..8010610e23 100644 --- a/frontend/control-center/src/pages/Webhooks/index.module.scss +++ b/frontend/control-center/src/pages/Webhooks/index.module.scss @@ -1,8 +1,9 @@ @import 'assets/scss/fonts.scss'; @import 'assets/scss/colors.scss'; +@import 'assets/scss/animations.scss'; .webhooksWrapper { - background: white; + background: var(--color-background-white); border-radius: 10px; padding: 32px; margin: 88px 1.5em 0 191px; @@ -12,6 +13,13 @@ width: 100%; } +.headlineContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + .webhooksHeadline { @include font-xl; font-weight: 900; @@ -32,3 +40,60 @@ flex-direction: row; flex-wrap: wrap; } + +.listHeader { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + h2 { + @include font-base; + color: var(--color-text-gray); + font-weight: bold; + width: 25%; + } + + h2:first-child { + width: 40%; + } + + h2:last-child { + width: 10%; + } +} + +.successfullySubscribed { + @include font-base; + color: white; +} + +@keyframes translateYIn { + 0% { + transform: translateY(-50px); + opacity: 0; + } + + 50% { + transform: translateY(16px); + opacity: 1; + } + + 100% { + transform: translateY(-50px); + opacity: 0; + } +} + +.translateYAnimIn { + animation: translateYIn 4s ease-in-out; +} + +.animateIn { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.animateOut { + animation: fadeInTranslateXLeft 3000ms ease; +} diff --git a/frontend/control-center/src/pages/Webhooks/index.tsx b/frontend/control-center/src/pages/Webhooks/index.tsx index 109576124b..4f9279a993 100644 --- a/frontend/control-center/src/pages/Webhooks/index.tsx +++ b/frontend/control-center/src/pages/Webhooks/index.tsx @@ -1,21 +1,165 @@ -import React, {useEffect} from 'react'; +import {SettingsModal} from 'components/alerts/SettingsModal'; +import {Button} from 'components/cta/Button'; +import {Webhook} from 'model/Webhook'; +import React, {useEffect, useState} from 'react'; +import {connect, ConnectedProps} from 'react-redux'; +import {listWebhooks, subscribeWebhook, updateWebhook} from '../../actions/webhook'; +import {StateModel} from '../../reducers'; import {setPageTitle} from '../../services/pageTitle'; import styles from './index.module.scss'; +import SubscriptionModal from './SubscriptionModal'; +import WebhooksListItem from './WebhooksListItem'; + +type WebhooksProps = {} & ConnectedProps; + +const mapStateToProps = (state: StateModel) => ({ + webhooks: Object.values(state.data.webhooks), +}); + +const mapDispatchToProps = { + listWebhooks, + subscribeWebhook, + updateWebhook, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const Webhooks = (props: WebhooksProps) => { + const {listWebhooks, webhooks} = props; + const [newWebhook, setNewWebhook] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [errorOccurred, setErrorOccurred] = useState(false); + const [showSuccessNotification, setShowSuccessNotification] = useState(false); + const [notificationText, setNotificatioNText] = useState(''); + const [notifcationColor, setNotifcationColor] = useState(''); -const Webhooks = () => { useEffect(() => { setPageTitle('Webhooks'); }, []); + useEffect(() => { + webhooks.length === 0 && listWebhooks(); + }, [webhooks]); + + const handleNotification = (show: boolean, error: boolean) => { + error + ? (setNotificatioNText('Error occurred'), setNotifcationColor('#d51548')) + : (setNotificatioNText('Successfully Subscribed!'), setNotifcationColor('#0da36b')); + setShowSuccessNotification(show); + }; + + const upsertWebhook = ( + isNew: boolean, + webhook: Webhook, + onCall?: () => void, + onResponse?: () => void, + onError?: (error: Error) => void + ) => { + onCall(); + if (isNew) { + props + .subscribeWebhook({...webhook}) + .then(() => onResponse()) + .catch((error: Error) => { + onError(error); + }); + } else { + props + .updateWebhook({...webhook, id: webhook.id}) + .then(() => onResponse()) + .catch((error: Error) => { + onError(error); + }); + } + }; + + const subscribeWebhookConfirm = (isNew: boolean, webhook: Webhook) => { + upsertWebhook( + isNew, + webhook, + () => { + setErrorOccurred(false); + setIsLoading(true); + }, + () => { + setIsLoading(false); + setNewWebhook(false); + }, + (error: Error) => { + console.error(error); + setErrorOccurred(true); + setIsLoading(false); + } + ); + }; + + const SuccessfulSubscribed = () => { + return ( +
+ {notificationText} +
+ ); + }; + return ( -
-
+ <> + {showSuccessNotification && } + {newWebhook && ( + setNewWebhook(false)} title="Subscribe Webhook" style={{fontSize: '40px'}}> + + + )} +
+
+
+

Webhooks

+ +
+
+
+

URL

+

Name

+

Events

+

Status

+
-

Webhooks

+ {webhooks && + webhooks.map((webhook: Webhook, index) => ( + + ))}
-
+ ); }; -export default Webhooks; +export default connector(Webhooks); diff --git a/frontend/control-center/src/reducers/data/index.ts b/frontend/control-center/src/reducers/data/index.ts index 79eeda13d4..a9d7f2ae3f 100644 --- a/frontend/control-center/src/reducers/data/index.ts +++ b/frontend/control-center/src/reducers/data/index.ts @@ -4,6 +4,8 @@ import {User} from 'model'; import user from './user'; import config, {Config} from './config'; import channels, {ChannelsState} from './channels'; +import {Webhook} from 'model/Webhook'; +import webhooks from './webhooks'; export * from './channels'; export * from './config'; @@ -13,12 +15,14 @@ export type DataState = { user: User; channels: ChannelsState; config: Config; + webhooks: Webhook; }; const reducers: Reducer = combineReducers({ user, channels, config, + webhooks, }); export default reducers; diff --git a/frontend/control-center/src/reducers/data/webhooks/index.ts b/frontend/control-center/src/reducers/data/webhooks/index.ts new file mode 100644 index 0000000000..48c2981cc6 --- /dev/null +++ b/frontend/control-center/src/reducers/data/webhooks/index.ts @@ -0,0 +1,40 @@ +import {ActionType, getType} from 'typesafe-actions'; +import * as actions from '../../../actions/webhook'; +import {keyBy} from 'lodash-es'; + +type Action = ActionType; + +const webhookReducer: any = (state = {}, action: Action) => { + switch (action.type) { + case getType(actions.saveWebhooks): + return { + ...state, + ...keyBy(action.payload, 'id'), + }; + case getType(actions.enableWebhook): + return { + ...state, + [action.payload.id]: { + ...action.payload, + }, + }; + case getType(actions.disableWebhook): + return { + ...state, + [action.payload.id]: { + ...action.payload, + }, + }; + case getType(actions.changeWebhook): + return { + ...state, + [action.payload.id]: { + ...action.payload, + }, + }; + default: + return state; + } +}; + +export default webhookReducer; diff --git a/frontend/control-center/src/routes/routes.ts b/frontend/control-center/src/routes/routes.ts index 43c4157523..c5e25b27d6 100644 --- a/frontend/control-center/src/routes/routes.ts +++ b/frontend/control-center/src/routes/routes.ts @@ -1,12 +1,31 @@ export const ROOT_ROUTE = '/'; -export const CHANNELS_ROUTE = '/channels'; -export const CHANNELS_CONNECTED_ROUTE = '/channels/connected'; -export const CHANNELS_FACEBOOK_ROUTE = '/channels/facebook'; -export const CHANNELS_CHAT_PLUGIN_ROUTE = '/channels/chatplugin'; -export const CHANNELS_TWILIO_SMS_ROUTE = '/channels/twilio.sms'; -export const CHANNELS_TWILIO_WHATSAPP_ROUTE = '/channels/twilio.whatsapp'; -export const CHANNELS_GOOGLE_ROUTE = '/channels/google'; -export const CHANNELS_INSTAGRAM_ROUTE = '/channels/instagram'; + +export const CONNECTORS_ROUTE = '/connectors'; +export const CONNECTORS_CONNECTED_ROUTE = '/connectors/connected'; +export const CONNECTORS_FACEBOOK_ROUTE = '/connectors/facebook'; +export const CONNECTORS_CHAT_PLUGIN_ROUTE = '/connectors/chatplugin'; +export const CONNECTORS_TWILIO_SMS_ROUTE = '/connectors/twilio.sms'; +export const CONNECTORS_TWILIO_WHATSAPP_ROUTE = '/connectors/twilio.whatsapp'; +export const CONNECTORS_GOOGLE_ROUTE = '/connectors/google'; +export const CONNECTORS_INSTAGRAM_ROUTE = '/connectors/instagram'; + export const CATALOG_ROUTE = '/catalog'; +export const CATALOG_CONNECTED_ROUTE = '/catalog/connected'; +export const CATALOG_FACEBOOK_ROUTE = '/catalog/facebook'; +export const CATALOG_CHAT_PLUGIN_ROUTE = '/catalog/chatplugin'; +export const CATALOG_TWILIO_SMS_ROUTE = '/catalog/twilio.sms'; +export const CATALOG_TWILIO_WHATSAPP_ROUTE = '/catalog/twilio.whatsapp'; +export const CATALOG_GOOGLE_ROUTE = '/catalog/google'; +export const CATALOG_INSTAGRAM_ROUTE = '/catalog/instagram'; + +export const INBOX_ROUTE = '/inbox'; +export const INBOX_CONNECTED_ROUTE = '/inbox/connected'; +export const INBOX_FACEBOOK_ROUTE = '/inbox/facebook'; +export const INBOX_CHAT_PLUGIN_ROUTE = '/inbox/chatplugin'; +export const INBOX_TWILIO_SMS_ROUTE = '/inbox/twilio.sms'; +export const INBOX_TWILIO_WHATSAPP_ROUTE = '/inbox/twilio.whatsapp'; +export const INBOX_GOOGLE_ROUTE = '/inbox/google'; +export const INBOX_INSTAGRAM_ROUTE = '/inbox/instagram'; + export const STATUS_ROUTE = '/status'; export const WEBHOOKS_ROUTE = '/webhooks'; diff --git a/frontend/control-center/src/services/pageTitle.ts b/frontend/control-center/src/services/pageTitle.ts index 2e67160a4a..7e49048693 100644 --- a/frontend/control-center/src/services/pageTitle.ts +++ b/frontend/control-center/src/services/pageTitle.ts @@ -1,7 +1,7 @@ export const setPageTitle = (title?: string) => { if (title?.length) { - document.title = `Airy UI - ${title}`; + document.title = `Control Center - ${title}`; } else { - document.title = 'Airy UI'; + document.title = 'Control Center'; } }; diff --git a/frontend/inbox/index.html b/frontend/inbox/index.html index 30bf3ed067..d3cd9c809e 100644 --- a/frontend/inbox/index.html +++ b/frontend/inbox/index.html @@ -1,5 +1,5 @@ - + Airy UI diff --git a/frontend/inbox/src/App.module.scss b/frontend/inbox/src/App.module.scss index ee153c151e..1d1f9e01df 100644 --- a/frontend/inbox/src/App.module.scss +++ b/frontend/inbox/src/App.module.scss @@ -20,4 +20,5 @@ height: 100%; min-height: 100vh; background-color: var(--color-background-gray); + transition: all 0.5 ease; } diff --git a/frontend/inbox/src/App.tsx b/frontend/inbox/src/App.tsx index f47b308b35..d903134957 100644 --- a/frontend/inbox/src/App.tsx +++ b/frontend/inbox/src/App.tsx @@ -23,6 +23,9 @@ const connector = connect(null, mapDispatchToProps); const App = (props: ConnectedProps) => { useEffect(() => { props.getClientConfig(); + if (localStorage.getItem('theme') === 'dark') { + document.documentElement.setAttribute('data-theme', 'dark'); + } }, []); return ( diff --git a/frontend/inbox/src/assets/animations/animations.scss b/frontend/inbox/src/assets/animations/animations.scss deleted file mode 100644 index a8a770a042..0000000000 --- a/frontend/inbox/src/assets/animations/animations.scss +++ /dev/null @@ -1,47 +0,0 @@ -@keyframes fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -@keyframes fadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} - -@keyframes fadeInTranslateXLeft { - from { - opacity: 0; - } - from { - transform: translateX(-100px); - } - to { - opacity: 1; - } - to { - transform: translateX(0px); - } -} - -@keyframes fadeOutTranslateXLeft { - from { - opacity: 1; - } - from { - transform: translateX(0px); - } - to { - opacity: 0; - } - to { - transform: translateX(-100px); - } -} diff --git a/frontend/inbox/src/assets/animations/index.ts b/frontend/inbox/src/assets/animations/index.ts deleted file mode 100644 index 02b548b4de..0000000000 --- a/frontend/inbox/src/assets/animations/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {Dispatch, SetStateAction} from 'react'; - -export const useAnimation = ( - useState: Dispatch>, - currentState: boolean, - fadeState: Dispatch>, - timeOut: number -) => { - setTimeout(() => useState(!currentState), timeOut); - setTimeout(() => fadeState(true), timeOut); - fadeState(false); -}; diff --git a/frontend/inbox/src/assets/scss/animations.scss b/frontend/inbox/src/assets/scss/animations.scss index f146b6c0d6..99198b81ba 100644 --- a/frontend/inbox/src/assets/scss/animations.scss +++ b/frontend/inbox/src/assets/scss/animations.scss @@ -87,7 +87,7 @@ .fill-to-bottom:hover, .fill-to-bottom:focus, .fill-to-bottom:active { - color: white; + color: var(--color-background-white); } .fill-to-bottom:hover:before, .fill-to-bottom:focus:before, @@ -123,7 +123,7 @@ .fill-to-top:hover, .fill-to-top:focus, .fill-to-top:active { - color: white; + color: var(--color-background-white); } .fill-to-top:hover:before, .fill-to-top:focus:before, diff --git a/frontend/inbox/src/components/ChannelAvatar/index.module.scss b/frontend/inbox/src/components/ChannelAvatar/index.module.scss index 1ef72c3c8a..70bd1c879b 100644 --- a/frontend/inbox/src/components/ChannelAvatar/index.module.scss +++ b/frontend/inbox/src/components/ChannelAvatar/index.module.scss @@ -13,4 +13,8 @@ width: 100%; border-radius: 50%; } + + circle { + fill: var(----color-channel-icon-circle); + } } diff --git a/frontend/inbox/src/components/ChannelAvatar/index.tsx b/frontend/inbox/src/components/ChannelAvatar/index.tsx index 7c71ac8da6..e11600d776 100644 --- a/frontend/inbox/src/components/ChannelAvatar/index.tsx +++ b/frontend/inbox/src/components/ChannelAvatar/index.tsx @@ -50,7 +50,7 @@ const ChannelAvatar = (props: ChannelAvatarProps) => { }; return ( -
+
{channel.metadata?.imageUrl || imageUrl ? getCustomLogo(channel) : getChannelAvatar(channel)}
); diff --git a/frontend/inbox/src/components/Dialog/index.module.scss b/frontend/inbox/src/components/Dialog/index.module.scss index e0e59e41ba..36116c2794 100644 --- a/frontend/inbox/src/components/Dialog/index.module.scss +++ b/frontend/inbox/src/components/Dialog/index.module.scss @@ -2,7 +2,7 @@ .dialog { position: absolute; - background-color: white; + background-color: var(--color-background-white); border: 1px solid var(--color-light-gray); border-radius: 4px; z-index: $popup; diff --git a/frontend/inbox/src/components/DialogCustomizable/index.module.scss b/frontend/inbox/src/components/DialogCustomizable/index.module.scss index e0e59e41ba..36116c2794 100644 --- a/frontend/inbox/src/components/DialogCustomizable/index.module.scss +++ b/frontend/inbox/src/components/DialogCustomizable/index.module.scss @@ -2,7 +2,7 @@ .dialog { position: absolute; - background-color: white; + background-color: var(--color-background-white); border: 1px solid var(--color-light-gray); border-radius: 4px; z-index: $popup; diff --git a/frontend/inbox/src/components/IconChannel/index.module.scss b/frontend/inbox/src/components/IconChannel/index.module.scss index db0a0ce0b9..c903742db0 100644 --- a/frontend/inbox/src/components/IconChannel/index.module.scss +++ b/frontend/inbox/src/components/IconChannel/index.module.scss @@ -1,3 +1,5 @@ +@import 'assets/scss/colors.scss'; + .iconName { display: flex; align-items: center; @@ -33,6 +35,10 @@ overflow: hidden; text-overflow: ellipsis; } + + circle { + fill: var(----color-channel-icon-circle); + } } .iconText { diff --git a/frontend/inbox/src/components/Sidebar/index.module.scss b/frontend/inbox/src/components/Sidebar/index.module.scss index 001b600fce..f8bc6c9fa9 100644 --- a/frontend/inbox/src/components/Sidebar/index.module.scss +++ b/frontend/inbox/src/components/Sidebar/index.module.scss @@ -10,7 +10,7 @@ margin-top: 88px; width: 80px; height: auto; - background-color: white; + background-color: var(--color-background-white); } .linkSection { diff --git a/frontend/inbox/src/components/Sidebar/index.tsx b/frontend/inbox/src/components/Sidebar/index.tsx index bd520478a5..cc95b13eda 100644 --- a/frontend/inbox/src/components/Sidebar/index.tsx +++ b/frontend/inbox/src/components/Sidebar/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {Link, matchPath, useLocation} from 'react-router-dom'; +import {Link, useMatch} from 'react-router-dom'; import {ReactComponent as InboxIcon} from 'assets/images/icons/inbox.svg'; import {ReactComponent as TagIcon} from 'assets/images/icons/priceTag.svg'; @@ -9,9 +9,8 @@ import {INBOX_ROUTE, TAGS_ROUTE} from '../../routes/routes'; import styles from './index.module.scss'; export const Sidebar = () => { - const location = useLocation(); const isActive = (route: string) => { - return !!matchPath(location.pathname, route); + return useMatch(`${route}/*`); }; return ( diff --git a/frontend/inbox/src/components/Tag/index.module.scss b/frontend/inbox/src/components/Tag/index.module.scss index c301335bc2..c247ece515 100644 --- a/frontend/inbox/src/components/Tag/index.module.scss +++ b/frontend/inbox/src/components/Tag/index.module.scss @@ -10,7 +10,7 @@ align-items: center; padding: 0px 8px; background: var(--color-airy-blue); - color: #fff; + color: var(--color-background-white); border-radius: 16px; line-height: 24px; max-width: 100%; @@ -39,7 +39,7 @@ height: 10px; width: 10px; path { - fill: #fff; + fill: white; } } diff --git a/frontend/inbox/src/components/Tag/index.tsx b/frontend/inbox/src/components/Tag/index.tsx index 55d1cd6145..8180e32f3f 100644 --- a/frontend/inbox/src/components/Tag/index.tsx +++ b/frontend/inbox/src/components/Tag/index.tsx @@ -1,11 +1,9 @@ import React from 'react'; import {connect, ConnectedProps} from 'react-redux'; - +import {StateModel} from '../../reducers'; import {Tag as TagModel} from 'model'; - import {ReactComponent as Close} from 'assets/images/icons/close.svg'; import styles from './index.module.scss'; -import {StateModel} from '../../reducers'; type TagProps = { tag: TagModel; @@ -38,7 +36,7 @@ export const Tag = ({tag, expanded, variant, onClick, removeTag, config: {tagCon color: `#${tagColor.font}`, border: `1px solid #${tagColor.border}`, } - : {backgroundColor: `#${tagColor.default}`}; + : {backgroundColor: `#${tagColor.default}`, color: 'white', border: `1px solid #${tagColor.border}`}; return (
diff --git a/frontend/inbox/src/components/TopBar/index.module.scss b/frontend/inbox/src/components/TopBar/index.module.scss index ad989f74cc..aa489d6548 100644 --- a/frontend/inbox/src/components/TopBar/index.module.scss +++ b/frontend/inbox/src/components/TopBar/index.module.scss @@ -1,6 +1,7 @@ @import 'assets/scss/colors.scss'; @import 'assets/scss/fonts.scss'; @import 'assets/scss/z-index.scss'; +@import 'assets/scss/animations.scss'; .topBar { display: flex; @@ -8,8 +9,8 @@ justify-content: space-between; z-index: $navigation; height: 72px; - background-color: white; - box-shadow: 0 3px 8px 0 var(--color-light-gray); + background-color: var(--color-background-white); + box-shadow: 0 3px 8px 0 var(--color-shadow-gray); position: fixed; overflow: visible; top: 0; @@ -56,16 +57,24 @@ } .dropHintOpen { + transition: 300ms ease; margin-bottom: 0; transform: rotate(180deg); } +.dropHintClose { + transition: 300ms ease; + margin-bottom: 0; + transform: rotate(0deg); +} + .accountDetails { padding-right: 12px; } .accountName { font-weight: 900; + color: var(--color-text-contrast); } .accountHint { @@ -82,7 +91,7 @@ .dropdownContainer { position: absolute; - background-color: white; + background-color: var(--color-background-white); border: 1px solid var(--color-light-gray); border-radius: 8px; top: 68px; @@ -167,6 +176,7 @@ .help { @include font-m; + background: transparent; margin-right: 16px; border: 1px solid var(--color-text-gray); border-radius: 50%; @@ -186,6 +196,12 @@ } } +.theme { + background: transparent; + border: none; + margin-left: 8px; +} + #dropDownLink { color: var(--color-airy-blue); text-decoration: none; @@ -227,3 +243,11 @@ } } } + +.animateIn { + animation: topbarDropdownIn 300ms ease; +} + +.animateOut { + animation: topbarDropdownOut 300ms ease; +} diff --git a/frontend/inbox/src/components/TopBar/index.tsx b/frontend/inbox/src/components/TopBar/index.tsx index 63e1d04dc8..35e28676c4 100644 --- a/frontend/inbox/src/components/TopBar/index.tsx +++ b/frontend/inbox/src/components/TopBar/index.tsx @@ -2,13 +2,16 @@ import React, {useState, useCallback} from 'react'; import {connect, ConnectedProps} from 'react-redux'; import {ListenOutsideClick} from 'components'; import {StateModel} from '../../reducers'; +import {Toggle} from 'components'; import {ReactComponent as ShortcutIcon} from 'assets/images/icons/shortcut.svg'; import {ReactComponent as LogoutIcon} from 'assets/images/icons/signOut.svg'; import {ReactComponent as AiryLogoWithText} from 'assets/images/logo/airyPrimaryRgb.svg'; +import {ReactComponent as AiryLogoWithTextDark} from 'assets/images/logo/airyLogoDark.svg'; import {ReactComponent as ChevronDownIcon} from 'assets/images/icons/chevronDown.svg'; import {ReactComponent as AiryLogo} from 'assets/images/logo/airyLogo.svg'; import styles from './index.module.scss'; import {env} from '../../env'; +import {useAnimation} from 'render'; interface TopBarProps { isAdmin: boolean; @@ -27,105 +30,129 @@ const connector = connect(mapStateToProps); const TopBar = (props: TopBarProps & ConnectedProps) => { const [isAccountDropdownOn, setAccountDropdownOn] = useState(false); const [isFaqDropdownOn, setFaqDropdownOn] = useState(false); + const [darkTheme, setDarkTheme] = useState(localStorage.getItem('theme') === 'dark' ? true : false); + const [animationAction, setAnimationAction] = useState(false); + const [chevronAnim, setChevronAnim] = useState(false); - const accountClickHandler = useCallback(() => { - setAccountDropdownOn(!isAccountDropdownOn); + const toggleAccountDropdown = useCallback(() => { + useAnimation(isAccountDropdownOn, setAccountDropdownOn, setAnimationAction, 300); + setChevronAnim(!chevronAnim); }, [setAccountDropdownOn, isAccountDropdownOn]); - const hideAccountDropdown = useCallback(() => { - setAccountDropdownOn(false); - }, [setAccountDropdownOn]); - - const faqClickHandler = useCallback(() => { - setFaqDropdownOn(!isFaqDropdownOn); + const toggleFaqDropdown = useCallback(() => { + useAnimation(isFaqDropdownOn, setFaqDropdownOn, setAnimationAction, 300); }, [setFaqDropdownOn, isFaqDropdownOn]); - const hideFaqDropdown = useCallback(() => { - setFaqDropdownOn(false); - }, [setFaqDropdownOn]); + const toggleDarkTheme = () => { + if (localStorage.getItem('theme') === 'dark') { + document.documentElement.removeAttribute('data-theme'); + localStorage.removeItem('theme'); + setDarkTheme(false); + } else { + localStorage.setItem('theme', 'dark'); + document.documentElement.setAttribute('data-theme', 'dark'); + setDarkTheme(true); + } + }; return (
- + {!darkTheme ? ( + + ) : ( + + )}
-
+
?
- {isFaqDropdownOn && ( - - {props.user.name && (
-
+
{props.user.name}
-
+
- {isAccountDropdownOn && ( - - )} +
); diff --git a/frontend/inbox/src/components/Wrapper/index.module.scss b/frontend/inbox/src/components/Wrapper/index.module.scss index 05cdd3f2da..5496a588ec 100644 --- a/frontend/inbox/src/components/Wrapper/index.module.scss +++ b/frontend/inbox/src/components/Wrapper/index.module.scss @@ -13,7 +13,7 @@ .Content { width: auto; - background: white; + background: var(--color-background-white); padding: 32px; margin: 88px 2.5em 5em 7.5em; border-radius: 10px; diff --git a/frontend/inbox/src/pages/Inbox/ConversationList/index.module.scss b/frontend/inbox/src/pages/Inbox/ConversationList/index.module.scss index 3b63823f9f..c97bfc7605 100644 --- a/frontend/inbox/src/pages/Inbox/ConversationList/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/ConversationList/index.module.scss @@ -6,7 +6,7 @@ width: 100%; overflow: hidden; height: 100%; - background-color: white; + background-color: var(--color-background-white); } .conversationListPaginationWrapper { @@ -16,7 +16,7 @@ .conversationListContainerFilterBox { display: block; - background: white; + background: var(--color-background-white); } .conversationListContactList { @@ -24,7 +24,7 @@ flex-direction: column; overflow-y: auto; height: 100%; - background-color: white; + background-color: var(--color-background-white); } .conversationListLoading { @@ -33,8 +33,6 @@ } .conversationListContainer { - box-shadow: 0 8px 20px -7px rgba(250, 245, 250, 0.7); - background: #fefefe; padding-bottom: 16px; margin: 16px; border-bottom: 1px solid var(--color-background-gray); diff --git a/frontend/inbox/src/pages/Inbox/ConversationListHeader/index.module.scss b/frontend/inbox/src/pages/Inbox/ConversationListHeader/index.module.scss index 39dfbf144a..ac105edb7d 100644 --- a/frontend/inbox/src/pages/Inbox/ConversationListHeader/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/ConversationListHeader/index.module.scss @@ -48,7 +48,7 @@ .backButton { border: none; - background-color: white; + background-color: var(--color-background-white); padding-left: 0px; padding-right: 14px; } @@ -59,14 +59,14 @@ .searchBox { color: black; - background: white; + background: var(--color-background-white); display: flex; align-items: center; } .searchButton { border: none; - background-color: white; + background-color: var(--color-background-white); cursor: pointer; outline: none; height: 24px; @@ -84,6 +84,7 @@ font-size: 24px; font-weight: 900; margin-bottom: 4px; + color: var(--color-text-contrast); } .searchIcon { diff --git a/frontend/inbox/src/pages/Inbox/ConversationListItem/index.module.scss b/frontend/inbox/src/pages/Inbox/ConversationListItem/index.module.scss index b706c2dafd..4ca38d41af 100644 --- a/frontend/inbox/src/pages/Inbox/ConversationListItem/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/ConversationListItem/index.module.scss @@ -54,7 +54,7 @@ .contactDetails { width: 100%; - border-bottom: 1px solid var(--color-background-gray); + border-bottom: 1px solid transparent; overflow: hidden; padding-bottom: 16px; } diff --git a/frontend/inbox/src/pages/Inbox/MessageInput/AudioRecording/index.module.scss b/frontend/inbox/src/pages/Inbox/MessageInput/AudioRecording/index.module.scss index ca2052c519..c52e51c38a 100644 --- a/frontend/inbox/src/pages/Inbox/MessageInput/AudioRecording/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/MessageInput/AudioRecording/index.module.scss @@ -34,7 +34,7 @@ svg { path { - fill: white; + fill: var(--color-background-white); } } } @@ -45,7 +45,7 @@ svg { path { - stroke: white; + stroke: var(--color-background-white); } } } diff --git a/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.module.scss b/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.module.scss index 37478da9c7..50ea4c3506 100644 --- a/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.module.scss +++ b/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.module.scss @@ -79,7 +79,7 @@ @include font-s; position: absolute; background-color: var(--color-text-contrast); - color: white; + color: var(--color-background-white); border-radius: 4px; padding: 4px 8px; display: none; @@ -169,7 +169,7 @@ svg { path { - stroke: white; + stroke: var(--color-background-white); fill: none; } } diff --git a/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.tsx b/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.tsx index c99d11d4f5..9c0575d88c 100644 --- a/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.tsx +++ b/frontend/inbox/src/pages/Inbox/MessageInput/InputOptions.tsx @@ -180,7 +180,7 @@ export const InputOptions = (props: Props) => { )} {isShowingEmojiDrawer && (
- +
)} diff --git a/frontend/inbox/src/pages/Inbox/MessageInput/InputSelector.module.scss b/frontend/inbox/src/pages/Inbox/MessageInput/InputSelector.module.scss index a351053737..c0eee6e815 100644 --- a/frontend/inbox/src/pages/Inbox/MessageInput/InputSelector.module.scss +++ b/frontend/inbox/src/pages/Inbox/MessageInput/InputSelector.module.scss @@ -29,6 +29,10 @@ outline: none; z-index: 1; + svg { + fill: var(--color-text-contrast); + } + &:hover { border-color: var(--color-airy-blue); diff --git a/frontend/inbox/src/pages/Inbox/MessageInput/index.module.scss b/frontend/inbox/src/pages/Inbox/MessageInput/index.module.scss index a094234d69..e52ef61059 100644 --- a/frontend/inbox/src/pages/Inbox/MessageInput/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/MessageInput/index.module.scss @@ -34,7 +34,7 @@ &:hover { .chevronDown { path { - fill: white; + fill: var(--color-background-white); } } } @@ -112,10 +112,9 @@ position: relative; width: 40px; margin-left: 8px; - transition: 0.2s ease-in-out all; top: 1px; border: none; - background: white; + background: transparent; padding: 0; outline: none; cursor: pointer; @@ -130,7 +129,7 @@ border: none; width: 40px; @include font-s; - background: #cad5db; + background: var(--color-button-gray); font-weight: 400; color: #000; border-radius: 50%; @@ -155,7 +154,7 @@ padding: 10px; border-radius: 4px; z-index: 1; - background-color: var(--color-light-gray); + background-color: var(--color-tooltip-gray); p { @include font-s; diff --git a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/Expandable.tsx b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/Expandable.tsx index 074e84a2d4..a219b03a18 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/Expandable.tsx +++ b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/Expandable.tsx @@ -16,7 +16,7 @@ export const Expandable = (props: ExpandableProps) => { return (
{!collapse ? ( - + ) : ( )} diff --git a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.module.scss b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.module.scss index b8b4abaada..aed316b572 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.module.scss @@ -36,6 +36,7 @@ .infoName { word-break: break-all; + color: var(--color-text-contrast); } .infoLink:link, @@ -54,6 +55,7 @@ .container legend { @include font-s-bold; margin-bottom: 15px; + color: var(--color-text-contrast); } .container label { @@ -67,6 +69,8 @@ padding: 0; margin: 0; @include font-s; + background: var(--color-background-white); + color: var(--color-text-contrast); } .details { @@ -76,12 +80,14 @@ .detailName { font-weight: bold; margin-right: 3px; + color: var(--color-text-contrast); } .expandable { display: flex; align-items: center; cursor: pointer; + color: var(--color-text-contrast); } .saveButtonContainer { @@ -93,6 +99,9 @@ .arrowIcon { margin-right: 6px; + svg { + fill: var(--color-text-contrast); + } } .downIcon { @@ -100,9 +109,44 @@ height: 9px; } +.arrowRightIcon { + width: 8px; +} + .infoIcon { width: 12px; margin-right: 6px; fill: var(--color-text-contrast); overflow: visible; } + +.contactConversationList { + position: absolute; + bottom: 40px; + display: flex; + flex-direction: column; + color: var(--color-text-contrast); + + .iconsContainer { + display: flex; + flex-wrap: wrap; + } + + span { + @include font-s-bold; + display: inline-block; + } + + button { + width: 40px; + border: none; + background: transparent; + margin-top: 5px; + opacity: 1; + transition: opacity 0.25s ease; + + &:hover { + opacity: 0.3; + } + } +} diff --git a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.tsx b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.tsx index a6e4cd4d96..5117caf025 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.tsx +++ b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/ContactDetails/index.tsx @@ -4,10 +4,12 @@ import {getContactDetails, updateContactDetails} from '../../../../../actions'; import {StateModel} from '../../../../../reducers'; import {getInfoDetailsPayload, fillContactInfo} from './util'; import {UpdateContactDetailsRequestPayload} from 'httpclient/src'; -import {Contact} from 'model'; +import {Contact, Source} from 'model'; import {ContactInfoPoint} from './ContactInfoPoint'; import {Expandable} from './Expandable'; -import {Button} from 'components'; +import {Button, ConnectorAvatar} from 'components'; +import {Link} from 'react-router-dom'; +import {INBOX_CONVERSATIONS_ROUTE} from '../../../../../routes/routes'; import styles from './index.module.scss'; import {cyContactSaveButton} from 'handles'; @@ -32,6 +34,11 @@ type ContactDetailsProps = { getIsExpanded: (isExpanded: boolean) => void; } & ConnectedProps; +interface ConversationInfoForContact { + id: string; + connector: string; +} + const ContactDetails = (props: ContactDetailsProps) => { const { conversationId, @@ -53,6 +60,8 @@ const ContactDetails = (props: ContactDetailsProps) => { const [organization, setOrganization] = useState('company name'); const [newContactCollapsed, setNewContactCollapsed] = useState(existingContact); const [existingContactCollapsed, setExistingContactCollapsed] = useState(existingContact); + const [conversationsForContact, setConversationsForContact] = useState([]); + const [areOthersConversationForContact, setAreOthersConversationForContact] = useState(false); const [expanded, setExpanded] = useState(false); const totalInfoPoints = 6; const visibleInfoPointsNewContact = 1; @@ -64,12 +73,15 @@ const ContactDetails = (props: ContactDetailsProps) => { useEffect(() => { getContactDetails(conversationId); setExpanded(false); + setAreOthersConversationForContact(false); + setConversationsForContact([]); }, [conversationId]); useEffect(() => { if (conversationId && contacts && contacts[conversationId]) { fillContactInfo(contacts[conversationId], setEmail, setPhone, setTitle, setAddress, setCity, setOrganization); updateContactType(contacts[conversationId]); + setConversationsForContact(formatConversationsForContact(contacts[conversationId].conversations)); } }, [contacts, conversationId]); @@ -84,6 +96,22 @@ const ContactDetails = (props: ContactDetailsProps) => { } }, [editingCanceled]); + const formatConversationsForContact = (convObj: {[key: string]: string}) => { + const conversationsForContactArr = []; + + for (const idProperty in convObj) { + if (idProperty !== conversationId) { + setAreOthersConversationForContact(true); + const convInfo = {} as ConversationInfoForContact; + convInfo.id = idProperty; + convInfo.connector = convObj[idProperty]; + conversationsForContactArr.push(convInfo); + } + } + + return conversationsForContactArr; + }; + const removeDefaultTextWhenEditing = () => { if (email === 'email') setEmail(''); if (phone === 'phone') setPhone(''); @@ -138,46 +166,68 @@ const ContactDetails = (props: ContactDetailsProps) => { }; return ( -
-
- Contact - - - {(!newContactCollapsed || isEditing) && ( - <> - - - - {(expanded || isEditing) && ( - <> - - - - - )} - + <> + +
+ Contact + + + {(!newContactCollapsed || isEditing) && ( + <> + + + + {(expanded || isEditing) && ( + <> + + + + + )} + + )} +
+ + {isEditing ? ( +
+ +
+ ) : ( + )} -
- - {isEditing ? ( -
- + + + {areOthersConversationForContact && conversationsForContact && ( +
+ Other conversations for this contact: +
+ {conversationsForContact.map((conversationInfo: ConversationInfoForContact) => ( + + ))} +
- ) : ( - )} - + ); }; diff --git a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss index 2c3a0d93c0..b6730ef5f4 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss @@ -1,6 +1,6 @@ @import 'assets/scss/colors.scss'; @import 'assets/scss/fonts.scss'; -@import '../../../../assets/animations/animations.scss'; +@import 'assets/scss/animations.scss'; .content { display: flex; @@ -8,7 +8,7 @@ height: auto; flex-direction: column; overflow: auto; - background-color: #fff; + background-color: var(--color-background-white); margin: 0 0 0 8px; padding: 16px; border-top-left-radius: 8px; @@ -38,9 +38,6 @@ &:hover { svg { visibility: visible; - path { - fill: var(--color-airy-blue); - } &:hover { cursor: pointer; } @@ -106,6 +103,7 @@ svg { height: 8px; margin-bottom: 0.5px; + fill: var(--color-soft-green); } } @@ -135,6 +133,7 @@ .tagsHeaderTitle { font-weight: bold; + color: var(--color-text-contrast); } .addTags { @@ -154,6 +153,7 @@ .addTagsDescription { margin: 8px 0; + color: var(--color-text-contrast); } .addTagsButtonRow { @@ -177,6 +177,12 @@ cursor: pointer; } +.cancelIcon { + svg { + fill: var(--color-text-contrast); + } +} + .iconBlue { svg { fill: var(--color-airy-blue); diff --git a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx index a124668e03..41264c4b29 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx +++ b/frontend/inbox/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx @@ -8,7 +8,7 @@ import {Avatar} from 'components'; import ColorSelector from '../../../../components/ColorSelector'; import Dialog from '../../../../components/Dialog'; import {StateModel, isComponentHealthy} from '../../../../reducers'; -import {useAnimation} from '../../../../assets/animations'; +import {useAnimation} from 'render'; import ContactDetails from './ContactDetails'; import styles from './index.module.scss'; @@ -145,7 +145,7 @@ const ConversationMetadata = (props: ConnectedProps) => { const cancelEditDisplayName = () => { setDisplayName(conversation.metadata.contact.displayName); - useAnimation(setShowEditDisplayName, showEditDisplayName, setFade, 400); + useAnimation(showEditDisplayName, setShowEditDisplayName, setFade, 400); }; const editDisplayName = () => { @@ -175,7 +175,7 @@ const ConversationMetadata = (props: ConnectedProps) => { return (
- useAnimation(setShowTagsDialog, showTagsDialog, setFade, 400)}> + useAnimation(showTagsDialog, setShowTagsDialog, setFade, 400)}>
) => { ) : ( - )} diff --git a/frontend/inbox/src/pages/Inbox/Messenger/MessageList/index.module.scss b/frontend/inbox/src/pages/Inbox/Messenger/MessageList/index.module.scss index da2faa3260..9a9481c9fd 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/MessageList/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/Messenger/MessageList/index.module.scss @@ -24,7 +24,7 @@ cursor: pointer; border: none; outline: none; - background-color: white; + background-color: var(--color-background-white); &:hover { .suggestionIcon { diff --git a/frontend/inbox/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss b/frontend/inbox/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss index 7e5a94ef51..49200a23b4 100644 --- a/frontend/inbox/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss @@ -6,7 +6,7 @@ flex: 1; height: auto; flex-direction: column; - background-color: #fff; + background-color: var(--color-background-white); margin: 0 8px 0 8px; border-top-left-radius: 8px; border-top-right-radius: 8px; diff --git a/frontend/inbox/src/pages/Inbox/NoConversations/index.module.scss b/frontend/inbox/src/pages/Inbox/NoConversations/index.module.scss index 0d8b1ab589..05efc6badc 100644 --- a/frontend/inbox/src/pages/Inbox/NoConversations/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/NoConversations/index.module.scss @@ -1,5 +1,7 @@ .component { margin: 1.5em; + background-color: var(--color-background-white); + color: var(--color-text-contrast); } .component { diff --git a/frontend/inbox/src/pages/Inbox/QuickFilter/Popup.module.scss b/frontend/inbox/src/pages/Inbox/QuickFilter/Popup.module.scss index 5b61b19209..830f101819 100644 --- a/frontend/inbox/src/pages/Inbox/QuickFilter/Popup.module.scss +++ b/frontend/inbox/src/pages/Inbox/QuickFilter/Popup.module.scss @@ -66,8 +66,9 @@ .filterButton { border: 1px solid var(--color-light-gray); + color: var(--color-text-contrast); border-radius: 8px; - background-color: white; + background-color: var(--color-background-white); padding: 6px 8px; margin-right: 8px; display: flex; @@ -76,7 +77,6 @@ outline: none; max-height: 29px; white-space: nowrap; - svg { display: inline-block; margin-right: 2px; @@ -102,16 +102,16 @@ @extend .filterButton; border: 1px solid var(--color-airy-blue); background-color: var(--color-airy-blue); - color: white; + color: var(--color-background-white); outline: none; path { - stroke: white; - fill: white; + stroke: var(--color-background-white); + fill: var(--color-background-white); } .openIcon { - border-color: white; + border-color: var(--color-background-white); } } @@ -157,7 +157,11 @@ flex-direction: row; margin: 0px; cursor: pointer; - border: 7px solid white; + border: 7px solid var(--color-background-white); + + circle { + fill: var(----color-channel-icon-circle); + } } .sourceSelected { @@ -196,7 +200,7 @@ margin-right: 4px; path { - fill: white; + fill: var(--color-background-white); } svg { @@ -211,6 +215,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: var(--color-text-contrast); } .checkmarkCircleIcon { diff --git a/frontend/inbox/src/pages/Inbox/QuickFilter/index.module.scss b/frontend/inbox/src/pages/Inbox/QuickFilter/index.module.scss index d77ae170f3..a4c9835fd1 100644 --- a/frontend/inbox/src/pages/Inbox/QuickFilter/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/QuickFilter/index.module.scss @@ -30,6 +30,7 @@ border: 1px solid transparent; cursor: pointer; transition: background 400ms, border 400ms; + color: var(--color-text-contrast); } .quickFilterButtonActive { @@ -67,7 +68,7 @@ display: flex; align-items: center; cursor: pointer; - background: rgba(0, 0, 0, 0); + background: var(--color-text-contrast); border: none; margin-right: -13px; margin-top: -12px; @@ -108,7 +109,7 @@ .filterHint { @include font-s; background-color: var(--color-airy-blue); - color: white; + color: var(--color-background-white); border-radius: 4px; padding: 4px 8px; margin: 0 0 4px 4px; diff --git a/frontend/inbox/src/pages/Inbox/TemplateSelector/index.module.scss b/frontend/inbox/src/pages/Inbox/TemplateSelector/index.module.scss index 6ec0129d03..866b98ef5f 100644 --- a/frontend/inbox/src/pages/Inbox/TemplateSelector/index.module.scss +++ b/frontend/inbox/src/pages/Inbox/TemplateSelector/index.module.scss @@ -50,6 +50,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: var(--color-text-contrast); } .emptyMessage { diff --git a/frontend/inbox/src/pages/Tags/EmptyStateTags.module.scss b/frontend/inbox/src/pages/Tags/EmptyStateTags.module.scss index b48382a4d8..076d4533ab 100644 --- a/frontend/inbox/src/pages/Tags/EmptyStateTags.module.scss +++ b/frontend/inbox/src/pages/Tags/EmptyStateTags.module.scss @@ -4,7 +4,7 @@ .cardRaised { width: 100%; height: 100%; - background: #fff; + background: var(--color-background-white); display: flex; flex-direction: column; align-content: center; diff --git a/frontend/inbox/src/pages/Tags/TableRow.module.scss b/frontend/inbox/src/pages/Tags/TableRow.module.scss index c8aaaa511e..8ed1194fb8 100644 --- a/frontend/inbox/src/pages/Tags/TableRow.module.scss +++ b/frontend/inbox/src/pages/Tags/TableRow.module.scss @@ -24,8 +24,9 @@ } .editInput { - border: 1px solid #cad5db; - background-color: #f7f7f7; + border: 1px solid var(--color-light-gray); + background-color: var(--color-background-white); + color: var(--color-text-contrast); border-radius: 4px; padding: 6px; font-size: 16px; diff --git a/frontend/inbox/src/pages/Tags/index.module.scss b/frontend/inbox/src/pages/Tags/index.module.scss index af3902a1b8..ebf4dd7816 100644 --- a/frontend/inbox/src/pages/Tags/index.module.scss +++ b/frontend/inbox/src/pages/Tags/index.module.scss @@ -34,7 +34,7 @@ .cardRaised { width: 100%; height: 100%; - background: #fff; + background: var(--color-background-white); display: flex; flex-direction: column; align-content: center; @@ -97,7 +97,7 @@ align-items: center; color: var(--color-airy-blue); cursor: pointer; - background: white; + background: var(--color-background-white); border: none; &:hover { color: var(--color-airy-blue-hover); @@ -120,6 +120,7 @@ p { margin-bottom: 8px; max-width: 480px; + color: var(--color-text-contrast); strong { font-weight: 700; word-wrap: break-word; diff --git a/frontend/inbox/src/services/pageTitle.ts b/frontend/inbox/src/services/pageTitle.ts index 2e67160a4a..81f62b9205 100644 --- a/frontend/inbox/src/services/pageTitle.ts +++ b/frontend/inbox/src/services/pageTitle.ts @@ -1,7 +1,7 @@ export const setPageTitle = (title?: string) => { if (title?.length) { - document.title = `Airy UI - ${title}`; + document.title = `Inbox - ${title}`; } else { - document.title = 'Airy UI'; + document.title = 'Inbox'; } }; diff --git a/go.mod b/go.mod index 008c7bbe30..101e6075e1 100644 --- a/go.mod +++ b/go.mod @@ -28,11 +28,14 @@ require ( github.com/txn2/txeh v1.3.0 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect goji.io v2.0.2+incompatible - gopkg.in/segmentio/analytics-go.v3 v3.1.0 // indirect + gopkg.in/segmentio/analytics-go.v3 v3.1.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.19.0 k8s.io/apimachinery v0.19.0 k8s.io/client-go v0.19.0 + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/gorilla/mux v1.8.0 // indirect + golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect k8s.io/klog v1.0.0 golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect diff --git a/go.sum b/go.sum index 44a59bdc76..d4f04ad68b 100644 --- a/go.sum +++ b/go.sum @@ -202,6 +202,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -257,6 +259,8 @@ github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyyc github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= @@ -564,6 +568,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= +golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -614,6 +620,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2l golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -653,6 +661,9 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -663,6 +674,8 @@ golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -760,6 +773,8 @@ gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/segmentio/analytics-go.v3 v3.1.0 h1:UzxH1uaGZRpMKDhJyBz0pexz6yUoBU3x8bJsRk/HV6U= gopkg.in/segmentio/analytics-go.v3 v3.1.0/go.mod h1:4QqqlTlSSpVlWA9/9nDcPw+FkM2yv1NQoYjUbL9/JAw= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/go_repositories.bzl b/go_repositories.bzl index 47e3479ad1..102e20a430 100644 --- a/go_repositories.bzl +++ b/go_repositories.bzl @@ -444,6 +444,13 @@ def go_repositories(): sum = "h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA=", version = "v0.0.0-20191227052852-215e87163ea7", ) + go_repository( + name = "com_github_golang_jwt_jwt", + importpath = "github.com/golang-jwt/jwt", + sum = "h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=", + version = "v3.2.2+incompatible", + ) + go_repository( name = "com_github_golang_mock", importpath = "github.com/golang/mock", @@ -521,6 +528,12 @@ def go_repositories(): sum = "h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=", version = "v0.0.0-20181017120253-0766667cb4d1", ) + go_repository( + name = "com_github_gorilla_mux", + importpath = "github.com/gorilla/mux", + sum = "h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=", + version = "v1.8.0", + ) go_repository( name = "com_github_gorilla_websocket", @@ -1477,8 +1490,8 @@ def go_repositories(): go_repository( name = "org_golang_x_crypto", importpath = "golang.org/x/crypto", - sum = "h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=", - version = "v0.0.0-20200622213623-75b288015ac9", + sum = "h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8=", + version = "v0.0.0-20220507011949-2cf3adece122", ) go_repository( name = "org_golang_x_exp", @@ -1516,8 +1529,8 @@ def go_repositories(): go_repository( name = "org_golang_x_net", importpath = "golang.org/x/net", - sum = "h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=", - version = "v0.0.0-20201110031124-69a78807bb2b", + sum = "h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=", + version = "v0.0.0-20211112202133-69e39bad7dc2", ) go_repository( name = "org_golang_x_oauth2", @@ -1535,15 +1548,21 @@ def go_repositories(): go_repository( name = "org_golang_x_sys", importpath = "golang.org/x/sys", - sum = "h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=", - version = "v0.0.0-20200930185726-fdedc70b468f", + sum = "h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=", + version = "v0.0.0-20210615035016-665e8c7367d1", + ) + go_repository( + name = "org_golang_x_term", + importpath = "golang.org/x/term", + sum = "h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=", + version = "v0.0.0-20201126162022-7de9c90e9dd1", ) go_repository( name = "org_golang_x_text", importpath = "golang.org/x/text", - sum = "h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=", - version = "v0.3.3", + sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=", + version = "v0.3.6", ) go_repository( name = "org_golang_x_time", diff --git a/infrastructure/controller/go.mod b/infrastructure/controller/go.mod index 51aa7efc2d..0d34eb53ad 100644 --- a/infrastructure/controller/go.mod +++ b/infrastructure/controller/go.mod @@ -5,6 +5,9 @@ go 1.16 require ( github.com/airyhq/airy/infrastructure/lib/go/k8s/handler v0.0.0 github.com/airyhq/airy/infrastructure/lib/go/k8s/util v0.0.0 + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/gorilla/mux v1.8.0 // indirect + golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect k8s.io/api v0.19.0 k8s.io/apimachinery v0.19.0 k8s.io/client-go v0.19.0 diff --git a/infrastructure/controller/go.sum b/infrastructure/controller/go.sum index 40e07dda73..c55a7b5f6d 100644 --- a/infrastructure/controller/go.sum +++ b/infrastructure/controller/go.sum @@ -56,6 +56,8 @@ github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nA github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -94,6 +96,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= @@ -149,6 +153,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= +golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -183,6 +189,7 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -209,11 +216,16 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -285,6 +297,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/infrastructure/controller/pkg/endpoints/BUILD b/infrastructure/controller/pkg/endpoints/BUILD index 5a5a032318..4570a935f9 100644 --- a/infrastructure/controller/pkg/endpoints/BUILD +++ b/infrastructure/controller/pkg/endpoints/BUILD @@ -3,12 +3,19 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "endpoints", - srcs = ["endpoints.go"], + srcs = [ + "auth.go", + "server.go", + "services.go", + ], importpath = "github.com/airyhq/airy/infrastructure/controller/pkg/endpoints", visibility = ["//visibility:public"], deps = [ + "@com_github_golang_jwt_jwt//:jwt", + "@com_github_gorilla_mux//:mux", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", "@io_k8s_client_go//kubernetes:go_default_library", + "@io_k8s_klog//:klog", ], ) diff --git a/infrastructure/controller/pkg/endpoints/auth.go b/infrastructure/controller/pkg/endpoints/auth.go new file mode 100644 index 0000000000..261f5146dc --- /dev/null +++ b/infrastructure/controller/pkg/endpoints/auth.go @@ -0,0 +1,108 @@ +package endpoints + +import ( + "context" + "encoding/base64" + "github.com/golang-jwt/jwt" + "k8s.io/klog" + "log" + "net/http" + "regexp" + "strings" +) + +type EnableAuthMiddleware struct { + pattern *regexp.Regexp +} + +// MustNewAuthMiddleware Only paths that match the regexp pattern will be authenticated +func MustNewAuthMiddleware(pattern string) EnableAuthMiddleware { + r, err := regexp.Compile(pattern) + if err != nil { + log.Fatal(err) + } + return EnableAuthMiddleware{pattern: r} +} + +func (a EnableAuthMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if !a.pattern.MatchString(r.URL.Path) { + next.ServeHTTP(w, r) + return + } + + // Auth middlewares attach a flag to the context indicating that authentication was successful + if val, ok := ctx.Value("auth").(bool); ok && val == true { + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) +} + +type SystemTokenMiddleware struct { + systemToken string +} + +func NewSystemTokenMiddleware(systemToken string) SystemTokenMiddleware { + return SystemTokenMiddleware{systemToken: systemToken} +} + +func (s SystemTokenMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authPayload := r.Header.Get("Authorization") + authPayload = strings.TrimPrefix(authPayload, "Bearer ") + + if authPayload == s.systemToken { + ctx := context.WithValue(r.Context(), "auth", true) + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + next.ServeHTTP(w, r) + }) +} + +type JwtMiddleware struct { + jwtSecret []byte +} + +func NewJwtMiddleware(jwtSecret string) JwtMiddleware { + data, err := base64.StdEncoding.DecodeString(jwtSecret) + if err != nil { + klog.Fatal("failed to base64 decode jwt secret: ", err) + } + + return JwtMiddleware{jwtSecret: data} +} + + +func (j JwtMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authPayload := r.Header.Get("Authorization") + authPayload = strings.TrimPrefix(authPayload, "Bearer ") + if authPayload == "" { + authPayload = getAuthCookie(r) + } + + token, err := jwt.Parse(authPayload, func(token *jwt.Token) (interface{}, error) { + return j.jwtSecret, nil + }) + + if err != nil || !token.Valid { + next.ServeHTTP(w, r) + return + } + + ctx := context.WithValue(r.Context(), "auth", true) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getAuthCookie(r *http.Request) string { + cookie, err := r.Cookie("airy_auth_token") + if err != nil { + return "" + } + return cookie.Value +} diff --git a/infrastructure/controller/pkg/endpoints/server.go b/infrastructure/controller/pkg/endpoints/server.go new file mode 100644 index 0000000000..7c38e053e2 --- /dev/null +++ b/infrastructure/controller/pkg/endpoints/server.go @@ -0,0 +1,42 @@ +package endpoints + +import ( + "github.com/gorilla/mux" + "k8s.io/klog" + "log" + "net/http" + "os" + + "k8s.io/client-go/kubernetes" +) + +func Serve(clientSet *kubernetes.Clientset, namespace string) { + r := mux.NewRouter() + + // Load authentication middleware only if auth env is present + authEnabled := false + systemToken := os.Getenv("systemToken") + if systemToken != "" { + klog.Info("adding system token auth") + middleware := NewSystemTokenMiddleware(systemToken) + r.Use(middleware.Middleware) + } + + jwtSecret := os.Getenv("jwtSecret") + if jwtSecret != "" { + klog.Info("adding jwt auth") + middleware := NewJwtMiddleware(jwtSecret) + r.Use(middleware.Middleware) + authEnabled = true + } + + if authEnabled { + authMiddleware := MustNewAuthMiddleware("/cluster") + r.Use(authMiddleware.Middleware) + } + + s := &Services{clientSet: clientSet, namespace: namespace} + r.Handle("/services", s) + + log.Fatal(http.ListenAndServe(":8080", r)) +} diff --git a/infrastructure/controller/pkg/endpoints/endpoints.go b/infrastructure/controller/pkg/endpoints/services.go similarity index 77% rename from infrastructure/controller/pkg/endpoints/endpoints.go rename to infrastructure/controller/pkg/endpoints/services.go index 7ad075e88e..6f68ed047d 100644 --- a/infrastructure/controller/pkg/endpoints/endpoints.go +++ b/infrastructure/controller/pkg/endpoints/services.go @@ -3,14 +3,12 @@ package endpoints import ( "context" "encoding/json" - "log" - "net/http" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "net/http" ) -type Server struct { +type Services struct { clientSet *kubernetes.Clientset namespace string } @@ -24,7 +22,7 @@ type Service struct { Component string `json:"component"` } -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (s *Services) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Only return apps that are part of a component deployments, _ := s.clientSet.AppsV1().Deployments(s.namespace).List(context.TODO(), v1.ListOptions{ LabelSelector: "core.airy.co/component", @@ -44,9 +42,3 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(resp) } - -func Serve(clientSet *kubernetes.Clientset, namespace string) { - s := &Server{clientSet: clientSet, namespace: namespace} - http.Handle("/services", s) - log.Fatal(http.ListenAndServe(":8080", nil)) -} diff --git a/infrastructure/helm-chart/BUILD b/infrastructure/helm-chart/BUILD index df61787f86..a68d6807bf 100644 --- a/infrastructure/helm-chart/BUILD +++ b/infrastructure/helm-chart/BUILD @@ -2,12 +2,11 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar") load("@com_github_airyhq_bazel_tools//helm:helm.bzl", "helm_template_test") load("//tools/build:helm.bzl", "helm_push") - filegroup( name = "files", srcs = glob( ["**/*"], - exclude = [ "BUILD" ], + exclude = ["BUILD"], ), ) @@ -20,7 +19,6 @@ pkg_tar( "//infrastructure/helm-chart/charts/prerequisites/charts/kafka:files", "//infrastructure/helm-chart/charts/tools/charts/akhq:files", "//infrastructure/helm-chart/charts/tools/charts/kafka-connect:files", - ], extension = "tgz", strip_prefix = "./", diff --git a/infrastructure/helm-chart/charts/components/charts/sources/charts/facebook/templates/service.yaml b/infrastructure/helm-chart/charts/components/charts/sources/charts/facebook/templates/service.yaml index 6b4f02fa0a..ce10000c57 100644 --- a/infrastructure/helm-chart/charts/components/charts/sources/charts/facebook/templates/service.yaml +++ b/infrastructure/helm-chart/charts/components/charts/sources/charts/facebook/templates/service.yaml @@ -13,3 +13,19 @@ spec: type: NodePort selector: app: sources-facebook-connector +--- +apiVersion: v1 +kind: Service +metadata: + name: sources-facebook-events-router + labels: + core.airy.co/prometheus: spring +spec: + ports: + - name: web + port: 80 + targetPort: 8080 + protocol: TCP + type: NodePort + selector: + app: sources-facebook-events-router diff --git a/infrastructure/helm-chart/charts/components/charts/sources/charts/google/templates/service.yaml b/infrastructure/helm-chart/charts/components/charts/sources/charts/google/templates/service.yaml index 3196b024fc..089965c57e 100644 --- a/infrastructure/helm-chart/charts/components/charts/sources/charts/google/templates/service.yaml +++ b/infrastructure/helm-chart/charts/components/charts/sources/charts/google/templates/service.yaml @@ -13,3 +13,19 @@ spec: type: NodePort selector: app: sources-google-connector +--- +apiVersion: v1 +kind: Service +metadata: + name: sources-google-events-router + labels: + core.airy.co/prometheus: spring +spec: + ports: + - name: web + port: 80 + targetPort: 8080 + protocol: TCP + type: NodePort + selector: + app: sources-google-events-router diff --git a/infrastructure/helm-chart/charts/components/charts/sources/charts/twilio/templates/service.yaml b/infrastructure/helm-chart/charts/components/charts/sources/charts/twilio/templates/service.yaml index ccceaf2ae7..03a28d3dd0 100644 --- a/infrastructure/helm-chart/charts/components/charts/sources/charts/twilio/templates/service.yaml +++ b/infrastructure/helm-chart/charts/components/charts/sources/charts/twilio/templates/service.yaml @@ -13,3 +13,19 @@ spec: type: NodePort selector: app: sources-twilio-connector +--- +apiVersion: v1 +kind: Service +metadata: + name: sources-twilio-events-router + labels: + core.airy.co/prometheus: spring +spec: + ports: + - name: web + port: 80 + targetPort: 8080 + protocol: TCP + type: NodePort + selector: + app: sources-twilio-events-router diff --git a/infrastructure/helm-chart/charts/ingress-controller/BUILD b/infrastructure/helm-chart/charts/ingress-controller/BUILD index c308888987..1f602ed4a8 100644 --- a/infrastructure/helm-chart/charts/ingress-controller/BUILD +++ b/infrastructure/helm-chart/charts/ingress-controller/BUILD @@ -2,14 +2,13 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar") load("@com_github_airyhq_bazel_tools//helm:helm.bzl", "helm_template_test") load("//tools/build:helm.bzl", "helm_push") - filegroup( name = "files", - visibility = ["//visibility:public"], srcs = glob( ["**/*"], - exclude = [ "BUILD" ], + exclude = ["BUILD"], ), + visibility = ["//visibility:public"], ) pkg_tar( diff --git a/infrastructure/helm-chart/charts/prerequisites/charts/beanstalkd/BUILD b/infrastructure/helm-chart/charts/prerequisites/charts/beanstalkd/BUILD index 30aeddb343..a962005d28 100644 --- a/infrastructure/helm-chart/charts/prerequisites/charts/beanstalkd/BUILD +++ b/infrastructure/helm-chart/charts/prerequisites/charts/beanstalkd/BUILD @@ -2,14 +2,13 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar") load("@com_github_airyhq_bazel_tools//helm:helm.bzl", "helm_template_test") load("//tools/build:helm.bzl", "helm_push_version") - filegroup( name = "files", - visibility = ["//visibility:public"], srcs = glob( ["**/*"], - exclude = [ "BUILD" ], + exclude = ["BUILD"], ), + visibility = ["//visibility:public"], ) pkg_tar( @@ -26,5 +25,5 @@ helm_template_test( helm_push_version( chart = ":package", - version = "1.0" + version = "1.0", ) diff --git a/infrastructure/helm-chart/charts/prerequisites/charts/kafka/BUILD b/infrastructure/helm-chart/charts/prerequisites/charts/kafka/BUILD index 1f1fd08452..859b0226bd 100644 --- a/infrastructure/helm-chart/charts/prerequisites/charts/kafka/BUILD +++ b/infrastructure/helm-chart/charts/prerequisites/charts/kafka/BUILD @@ -2,14 +2,13 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar") load("@com_github_airyhq_bazel_tools//helm:helm.bzl", "helm_template_test") load("//tools/build:helm.bzl", "helm_push_version") - filegroup( name = "files", - visibility = ["//visibility:public"], srcs = glob( ["**/*"], - exclude = [ "BUILD" ], + exclude = ["BUILD"], ), + visibility = ["//visibility:public"], ) pkg_tar( @@ -26,5 +25,5 @@ helm_template_test( helm_push_version( chart = ":package", - version = "2.7.0" + version = "2.7.0", ) diff --git a/infrastructure/helm-chart/charts/tools/charts/akhq/BUILD b/infrastructure/helm-chart/charts/tools/charts/akhq/BUILD index 062bc547aa..8900f38491 100644 --- a/infrastructure/helm-chart/charts/tools/charts/akhq/BUILD +++ b/infrastructure/helm-chart/charts/tools/charts/akhq/BUILD @@ -2,14 +2,13 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar") load("@com_github_airyhq_bazel_tools//helm:helm.bzl", "helm_template_test") load("//tools/build:helm.bzl", "helm_push_version") - filegroup( name = "files", - visibility = ["//visibility:public"], srcs = glob( ["**/*"], - exclude = [ "BUILD" ], + exclude = ["BUILD"], ), + visibility = ["//visibility:public"], ) pkg_tar( @@ -26,5 +25,5 @@ helm_template_test( helm_push_version( chart = ":package", - version = "0.16.0" + version = "0.16.0", ) diff --git a/infrastructure/helm-chart/charts/tools/charts/kafka-connect/BUILD b/infrastructure/helm-chart/charts/tools/charts/kafka-connect/BUILD index 1f1fd08452..859b0226bd 100644 --- a/infrastructure/helm-chart/charts/tools/charts/kafka-connect/BUILD +++ b/infrastructure/helm-chart/charts/tools/charts/kafka-connect/BUILD @@ -2,14 +2,13 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar") load("@com_github_airyhq_bazel_tools//helm:helm.bzl", "helm_template_test") load("//tools/build:helm.bzl", "helm_push_version") - filegroup( name = "files", - visibility = ["//visibility:public"], srcs = glob( ["**/*"], - exclude = [ "BUILD" ], + exclude = ["BUILD"], ), + visibility = ["//visibility:public"], ) pkg_tar( @@ -26,5 +25,5 @@ helm_template_test( helm_push_version( chart = ":package", - version = "2.7.0" + version = "2.7.0", ) diff --git a/infrastructure/helm-chart/templates/controller/deployment.yaml b/infrastructure/helm-chart/templates/controller/deployment.yaml index e2436a88f1..a0408839d8 100644 --- a/infrastructure/helm-chart/templates/controller/deployment.yaml +++ b/infrastructure/helm-chart/templates/controller/deployment.yaml @@ -25,18 +25,21 @@ spec: serviceAccountName: airy-controller automountServiceAccountToken: true containers: - - name: app - image: "{{ .Values.global.containerRegistry}}/infrastructure/controller:{{ default .Chart.Version .Values.global.appImageTag }}" - imagePullPolicy: Always - env: - - name: LABEL_SELECTOR - value: "core.airy.co/managed=true" - - name: NAMESPACE - value: {{ .Release.Namespace }} - resources: - requests: - cpu: "50m" - memory: "32Mi" - limits: - cpu: "50m" - memory: "128Mi" + - name: app + image: "{{ .Values.global.containerRegistry}}/infrastructure/controller:{{ default .Chart.Version .Values.global.appImageTag }}" + imagePullPolicy: Always + envFrom: + - configMapRef: + name: security + env: + - name: LABEL_SELECTOR + value: "core.airy.co/managed=true" + - name: NAMESPACE + value: {{ .Release.Namespace }} + resources: + requests: + cpu: "50m" + memory: "32Mi" + limits: + cpu: "50m" + memory: "128Mi" diff --git a/infrastructure/lib/go/k8s/handler/go.sum b/infrastructure/lib/go/k8s/handler/go.sum index 6938c16d4e..3cbcbd45bf 100644 --- a/infrastructure/lib/go/k8s/handler/go.sum +++ b/infrastructure/lib/go/k8s/handler/go.sum @@ -35,7 +35,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= diff --git a/lib/typescript/assets/images/icons/arrowLeft.svg b/lib/typescript/assets/images/icons/arrowLeft.svg index ff4c2a0264..44f219f634 100644 --- a/lib/typescript/assets/images/icons/arrowLeft.svg +++ b/lib/typescript/assets/images/icons/arrowLeft.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/arrowRight.svg b/lib/typescript/assets/images/icons/arrowRight.svg index 5fb1330dfe..04e5c641e9 100644 --- a/lib/typescript/assets/images/icons/arrowRight.svg +++ b/lib/typescript/assets/images/icons/arrowRight.svg @@ -1,3 +1,3 @@ - + diff --git a/lib/typescript/assets/images/icons/catalogIcon.svg b/lib/typescript/assets/images/icons/catalogIcon.svg index 8eea7d3faf..1580d107ff 100644 --- a/lib/typescript/assets/images/icons/catalogIcon.svg +++ b/lib/typescript/assets/images/icons/catalogIcon.svg @@ -1,3 +1,3 @@ - + diff --git a/lib/typescript/assets/images/icons/checkmark.svg b/lib/typescript/assets/images/icons/checkmark.svg index 1445ff999c..b18eef14dc 100644 --- a/lib/typescript/assets/images/icons/checkmark.svg +++ b/lib/typescript/assets/images/icons/checkmark.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/chevronLeft.svg b/lib/typescript/assets/images/icons/chevronLeft.svg new file mode 100644 index 0000000000..04ca73b09e --- /dev/null +++ b/lib/typescript/assets/images/icons/chevronLeft.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/typescript/assets/images/icons/chevronRight.svg b/lib/typescript/assets/images/icons/chevronRight.svg new file mode 100644 index 0000000000..617758eae1 --- /dev/null +++ b/lib/typescript/assets/images/icons/chevronRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/typescript/assets/images/icons/close.svg b/lib/typescript/assets/images/icons/close.svg index 0adabf932b..9658253cb8 100644 --- a/lib/typescript/assets/images/icons/close.svg +++ b/lib/typescript/assets/images/icons/close.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/disconnectIcon.svg b/lib/typescript/assets/images/icons/disconnectIcon.svg new file mode 100644 index 0000000000..f33f6d8d6a --- /dev/null +++ b/lib/typescript/assets/images/icons/disconnectIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/typescript/assets/images/icons/fallbackMediaImage.svg b/lib/typescript/assets/images/icons/fallbackMediaImage.svg new file mode 100644 index 0000000000..cd36aa44ba --- /dev/null +++ b/lib/typescript/assets/images/icons/fallbackMediaImage.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/typescript/assets/images/icons/inboxIcon.svg b/lib/typescript/assets/images/icons/inboxIcon.svg new file mode 100644 index 0000000000..c4ac78b544 --- /dev/null +++ b/lib/typescript/assets/images/icons/inboxIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/typescript/assets/images/icons/leftArrowCircle.svg b/lib/typescript/assets/images/icons/leftArrowCircle.svg new file mode 100644 index 0000000000..662f680d49 --- /dev/null +++ b/lib/typescript/assets/images/icons/leftArrowCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/typescript/assets/images/icons/pencil.svg b/lib/typescript/assets/images/icons/pencil.svg index 1b139da83d..83799f435c 100644 --- a/lib/typescript/assets/images/icons/pencil.svg +++ b/lib/typescript/assets/images/icons/pencil.svg @@ -1,3 +1,3 @@ - + diff --git a/lib/typescript/assets/images/icons/search.svg b/lib/typescript/assets/images/icons/search.svg index b8343adc03..c9495dbee6 100644 --- a/lib/typescript/assets/images/icons/search.svg +++ b/lib/typescript/assets/images/icons/search.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/zendeskLogo.svg b/lib/typescript/assets/images/icons/zendeskLogo.svg index d57eab32a1..fc26ed397c 100644 --- a/lib/typescript/assets/images/icons/zendeskLogo.svg +++ b/lib/typescript/assets/images/icons/zendeskLogo.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/lib/typescript/assets/images/logo/airyLogoDark.svg b/lib/typescript/assets/images/logo/airyLogoDark.svg new file mode 100644 index 0000000000..b6c0d466a1 --- /dev/null +++ b/lib/typescript/assets/images/logo/airyLogoDark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib/typescript/assets/scss/animations.scss b/lib/typescript/assets/scss/animations.scss new file mode 100644 index 0000000000..b500d31c82 --- /dev/null +++ b/lib/typescript/assets/scss/animations.scss @@ -0,0 +1,90 @@ +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes fadeInTranslateXLeft { + from { + opacity: 0; + transform: translateX(-100px); + } + to { + opacity: 1; + transform: translateX(0px); + } +} + +@keyframes fadeOutTranslateXLeft { + from { + opacity: 1; + transform: translateX(0px); + } + to { + opacity: 0; + transform: translateX(-100px); + } +} + +@keyframes searchfieldAnimIn { + from { + opacity: 0; + transform: translateX(300px); + width: 0px; + } + to { + opacity: 1; + transform: translateX(0px); + width: 300px; + } +} + +@keyframes searchfieldAnimOut { + from { + opacity: 1; + transform: translateX(0px); + width: 300px; + } + to { + opacity: 0; + transform: translateX(300px); + width: 0px; + } +} + +@keyframes topbarDropdownIn { + 0% { + opacity: 0; + transform: translateY(-50px); + } + 100% { + opacity: 1; + transform: translateY(-37px); + } +} + +@keyframes topbarDropdownOut { + 0% { + opacity: 1; + transform: translateY(-37px); + } + 60% { + opacity: 0; + } + 100% { + opacity: 0; + transform: translateY(-50px); + } +} diff --git a/lib/typescript/assets/scss/colors.scss b/lib/typescript/assets/scss/colors.scss index 2e367162e7..b8ed79336a 100644 --- a/lib/typescript/assets/scss/colors.scss +++ b/lib/typescript/assets/scss/colors.scss @@ -1,15 +1,32 @@ :root { + //colors that change with darkmode (default is light mode) --color-text-contrast: #212428; + --color-background-gray: #f7f7f7; + --color-light-greyish-white: #efefef; + --color-toggle-grey: #e8eaea; + --color-blue-white: #f5f8fa; + --color-background-white: #fff; + --color-channel-icon-circle: #f1faff; + --color-template-highlight: #fff; + --color-tooltip-gray: #cad5db; + --color-background-blue: #f1faff; + --color-template-gray: #f0f0f0; + --color-shadow-gray: #cad5db; + --color-button-gray: #cad5db; + --color-airy-message-inbound: #f1faff; + --color-airy-message-outbound: #1578d4; + --color-elements-blue: #1578d4; + --color-airy-message-text-inbound: #000000; + + //colors that do not change with darkmode --color-text-gray: #737373; --color-dark-elements-gray: #98a4ab; --color-icons-gray: #a0abb2; --color-light-gray: #cad5db; - --color-background-gray: #f7f7f7; - --color-template-gray: #f0f0f0; - --color-light-greyish-white: #efefef; --color-greyish-white: #e8eaea; - --color-template-highlight: #fff; - --color-blue-white: #f5f8fa; + --color-very-light-grey: #e8eaea; + --color-blue-white-button: #f5f8fa; + --color-button-light-blue: #f1faff; --color-airy-dark-blue: #1b4469; --color-airy-logo-blue: #4bb3fd; --color-hover-blue: #337bb3; @@ -19,8 +36,8 @@ --color-airy-blue-pressed: #1b4469; --color-fb-cta: #1877f2; --color-fb-cta-border: #2281fd; - --color-background-blue: #f1faff; --color-red-alert: #d51548; + --color-red-info: #ee336c; --color-accent-magenta: #f7147d; --color-error-background: #fae6e9; --color-highlight-yellow: #fbbd54; @@ -29,8 +46,26 @@ --color-soft-green: #0da36b; --color-tag-green: #0e764f; --color-tag-purple: #730a80; - --color-airy-message-outbound: #1578d4; - --color-airy-message-inbound: #f1faff; --color-airy-message-text-outbound: #ffffff; - --color-airy-message-text-inbound: #000000; + --color-switch-unchecked-background: #efefef; +} + +html[data-theme='dark'] { + --color-text-contrast: #e8e6e3; + --color-background-gray: #1f201c; + --color-light-greyish-white: #1f201c; + --color-toggle-grey: #828484; + --color-blue-white: #1f201c; + --color-background-white: #272822; + --color-channel-icon-circle: #272822; + --color-template-highlight: #272822; + --color-tooltip-gray: #1f201c; + --color-background-blue: #243037; + --color-template-gray: #484848; + --color-shadow-gray: #4d5153; + --color-button-gray: #313537; + --color-airy-message-inbound: #1c1e1f; + --color-airy-message-outbound: #1160aa; + --color-elements-blue: #115fa8; + --color-airy-message-text-inbound: #ffffff; } diff --git a/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss b/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss index 9728b7d6cb..a3113b487a 100644 --- a/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss +++ b/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss @@ -25,14 +25,13 @@ position: absolute; right: 16px; top: 16px; + + svg { + fill: var(--color-text-contrast); + } } .closeIcon { width: 10px; height: 10px; - svg { - path { - stroke: #aaa; - } - } } diff --git a/lib/typescript/components/alerts/SettingsModal/ModalHeader.tsx b/lib/typescript/components/alerts/SettingsModal/ModalHeader.tsx index e265836a44..12b7cf4d7e 100644 --- a/lib/typescript/components/alerts/SettingsModal/ModalHeader.tsx +++ b/lib/typescript/components/alerts/SettingsModal/ModalHeader.tsx @@ -1,19 +1,22 @@ -import React from 'react'; +import React, {CSSProperties} from 'react'; import styles from './ModalHeader.module.scss'; import {ReactComponent as CloseIcon} from 'assets/images/icons/close.svg'; type ModalHeaderProps = { title: string; close: (event: React.MouseEvent) => void; + style?: CSSProperties; }; -const ModalHeader = ({title, close}: ModalHeaderProps) => { +const ModalHeader = ({title, close, style}: ModalHeaderProps) => { return (
-
{title}
+
+ {title} +
); }; diff --git a/lib/typescript/components/alerts/SettingsModal/index.tsx b/lib/typescript/components/alerts/SettingsModal/index.tsx index 837aa46359..59809b9c5c 100644 --- a/lib/typescript/components/alerts/SettingsModal/index.tsx +++ b/lib/typescript/components/alerts/SettingsModal/index.tsx @@ -1,10 +1,19 @@ -import React from 'react'; +import React, {CSSProperties} from 'react'; import Modal from 'react-modal'; import ModalHeader from './ModalHeader'; import styles from './style.module.scss'; -export const SettingsModal = ({close, title, children, style}) => { +type SettingsModalProps = { + close: () => void; + title: string; + children: any; + style?: CSSProperties; + className?: string; +}; + +export const SettingsModal = (props: SettingsModalProps) => { + const {close, title, children, style, className} = props; return ( { shouldCloseOnOverlayClick={true} onRequestClose={close} > -
- - -
{children}
+
+ + {children}
); diff --git a/lib/typescript/components/alerts/SettingsModal/style.module.scss b/lib/typescript/components/alerts/SettingsModal/style.module.scss index 1e731e241f..75db10f939 100644 --- a/lib/typescript/components/alerts/SettingsModal/style.module.scss +++ b/lib/typescript/components/alerts/SettingsModal/style.module.scss @@ -19,7 +19,7 @@ min-width: 450px; transform: translate(-50%, -50%); border: none; - background-color: white; + background-color: var(--color-background-white); border-radius: 7px; padding: 2em; outline: none; diff --git a/lib/typescript/components/cta/Button/index.tsx b/lib/typescript/components/cta/Button/index.tsx index 5740394ec1..1adb2f5f06 100644 --- a/lib/typescript/components/cta/Button/index.tsx +++ b/lib/typescript/components/cta/Button/index.tsx @@ -1,4 +1,4 @@ -import React, {ReactNode} from 'react'; +import React, {CSSProperties, ReactNode} from 'react'; import styles from './style.module.scss'; @@ -9,11 +9,12 @@ type ButtonProps = { type?: 'submit' | 'button' | 'reset'; disabled?: boolean; styleVariant?: styleVariantType; + style?: CSSProperties; tabIndex?: any; dataCy?: string; }; -export const Button = ({children, onClick, type, styleVariant, disabled, tabIndex, dataCy}: ButtonProps) => { +export const Button = ({children, onClick, type, styleVariant, disabled, tabIndex, dataCy, style}: ButtonProps) => { const styleFor = (variant: styleVariantType) => { switch (variant) { case 'small': @@ -36,6 +37,7 @@ export const Button = ({children, onClick, type, styleVariant, disabled, tabInde return (