|
@@ -1,14 +1,47 @@
|
|
|
-import React, { useContext, useRef, useState } from "react";
|
|
|
|
|
-import ConfirmOverlay from "../../../components/ConfirmOverlay";
|
|
|
|
|
|
|
+import React, { useContext, useMemo, useRef, useState } from "react";
|
|
|
|
|
+import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
|
+import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
+import axios from "axios";
|
|
|
|
|
+import { useFieldArray, useForm } from "react-hook-form";
|
|
|
import styled from "styled-components";
|
|
import styled from "styled-components";
|
|
|
-import { Context } from "../../../shared/Context";
|
|
|
|
|
-import api from "../../../shared/api";
|
|
|
|
|
|
|
+import { match } from "ts-pattern";
|
|
|
|
|
+import type { IterableElement } from "type-fest";
|
|
|
|
|
+import { z } from "zod";
|
|
|
|
|
|
|
|
-interface Props {
|
|
|
|
|
|
|
+import ConfirmOverlay from "components/ConfirmOverlay";
|
|
|
|
|
+import Loading from "components/Loading";
|
|
|
|
|
+import Button from "components/porter/Button";
|
|
|
|
|
+import Container from "components/porter/Container";
|
|
|
|
|
+import { ControlledInput } from "components/porter/ControlledInput";
|
|
|
|
|
+import Dropdown from "components/porter/Dropdown";
|
|
|
|
|
+import Error from "components/porter/Error";
|
|
|
|
|
+import Icon from "components/porter/Icon";
|
|
|
|
|
+import SelectableList from "components/porter/SelectableList";
|
|
|
|
|
+import Spacer from "components/porter/Spacer";
|
|
|
|
|
+import Tag from "components/porter/Tag";
|
|
|
|
|
+import Text from "components/porter/Text";
|
|
|
|
|
+import {
|
|
|
|
|
+ emptyNotificationConfig,
|
|
|
|
|
+ notificationConfigFormValidator,
|
|
|
|
|
+ type NotificationConfigFormData,
|
|
|
|
|
+} from "lib/notifications/types";
|
|
|
|
|
+
|
|
|
|
|
+import api from "shared/api";
|
|
|
|
|
+import { Context } from "shared/Context";
|
|
|
|
|
+import hash from "assets/hash-02.svg";
|
|
|
|
|
+import save from "assets/save-01.svg";
|
|
|
|
|
+
|
|
|
|
|
+type SlackIntegrationListProps = {
|
|
|
slackData: any[];
|
|
slackData: any[];
|
|
|
-}
|
|
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
-const SlackIntegrationList: React.FC<Props> = (props) => {
|
|
|
|
|
|
|
+const statusOptions = [
|
|
|
|
|
+ { value: "successful", emoji: "✅", label: "Successful" },
|
|
|
|
|
+ { value: "failed", emoji: "⚠️", label: "Failed" },
|
|
|
|
|
+ { value: "progressing", emoji: "🚀", label: "Progressing" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const SlackIntegrationList: React.FC<SlackIntegrationListProps> = (props) => {
|
|
|
const [isDelete, setIsDelete] = useState(false);
|
|
const [isDelete, setIsDelete] = useState(false);
|
|
|
const [deleteIndex, setDeleteIndex] = useState(-1); // guaranteed to be set when used
|
|
const [deleteIndex, setDeleteIndex] = useState(-1); // guaranteed to be set when used
|
|
|
const { currentProject, setCurrentError } = useContext(Context);
|
|
const { currentProject, setCurrentError } = useContext(Context);
|
|
@@ -38,53 +71,32 @@ const SlackIntegrationList: React.FC<Props> = (props) => {
|
|
|
<ConfirmOverlay
|
|
<ConfirmOverlay
|
|
|
show={isDelete}
|
|
show={isDelete}
|
|
|
message={
|
|
message={
|
|
|
- deleteIndex != -1 &&
|
|
|
|
|
|
|
+ deleteIndex !== -1 &&
|
|
|
`Are you sure you want to delete the slack integration for team ${
|
|
`Are you sure you want to delete the slack integration for team ${
|
|
|
props.slackData[deleteIndex].team_name ||
|
|
props.slackData[deleteIndex].team_name ||
|
|
|
props.slackData[deleteIndex].team_id
|
|
props.slackData[deleteIndex].team_id
|
|
|
} in channel ${props.slackData[deleteIndex].channel}?`
|
|
} in channel ${props.slackData[deleteIndex].channel}?`
|
|
|
}
|
|
}
|
|
|
onYes={handleDelete}
|
|
onYes={handleDelete}
|
|
|
- onNo={() => setIsDelete(false)}
|
|
|
|
|
|
|
+ onNo={() => {
|
|
|
|
|
+ setIsDelete(false);
|
|
|
|
|
+ }}
|
|
|
/>
|
|
/>
|
|
|
<StyledIntegrationList>
|
|
<StyledIntegrationList>
|
|
|
{props.slackData?.length > 0 ? (
|
|
{props.slackData?.length > 0 ? (
|
|
|
props.slackData.map((inst, idx) => {
|
|
props.slackData.map((inst, idx) => {
|
|
|
if (deleted.current.has(idx)) return null;
|
|
if (deleted.current.has(idx)) return null;
|
|
|
return (
|
|
return (
|
|
|
- <Integration
|
|
|
|
|
- onClick={() => {}}
|
|
|
|
|
- disabled={false}
|
|
|
|
|
- key={`${inst.team_id}-${inst.channel}`}
|
|
|
|
|
- >
|
|
|
|
|
- <MainRow disabled={false}>
|
|
|
|
|
- <Flex>
|
|
|
|
|
- <Icon src={inst.team_icon_url && inst.team_icon_url} />
|
|
|
|
|
- <Label>
|
|
|
|
|
- {inst.team_name || inst.team_id} - {inst.channel}
|
|
|
|
|
- </Label>
|
|
|
|
|
- </Flex>
|
|
|
|
|
- <MaterialIconTray disabled={false}>
|
|
|
|
|
- <i
|
|
|
|
|
- className="material-icons"
|
|
|
|
|
- onClick={() => {
|
|
|
|
|
- setDeleteIndex(idx);
|
|
|
|
|
- setIsDelete(true);
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- delete
|
|
|
|
|
- </i>
|
|
|
|
|
- <i
|
|
|
|
|
- className="material-icons"
|
|
|
|
|
- onClick={() => {
|
|
|
|
|
- window.open(inst.configuration_url, "_blank");
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- launch
|
|
|
|
|
- </i>
|
|
|
|
|
- </MaterialIconTray>
|
|
|
|
|
- </MainRow>
|
|
|
|
|
- </Integration>
|
|
|
|
|
|
|
+ <SlackIntegration
|
|
|
|
|
+ projectID={currentProject.id}
|
|
|
|
|
+ key={idx.toString()}
|
|
|
|
|
+ idx={idx}
|
|
|
|
|
+ deleteIndex={(index: number) => {
|
|
|
|
|
+ setDeleteIndex(index);
|
|
|
|
|
+ setIsDelete(true);
|
|
|
|
|
+ }}
|
|
|
|
|
+ inst={inst}
|
|
|
|
|
+ />
|
|
|
);
|
|
);
|
|
|
})
|
|
})
|
|
|
) : (
|
|
) : (
|
|
@@ -97,6 +109,267 @@ const SlackIntegrationList: React.FC<Props> = (props) => {
|
|
|
|
|
|
|
|
export default SlackIntegrationList;
|
|
export default SlackIntegrationList;
|
|
|
|
|
|
|
|
|
|
+type SlackIntegrationProps = {
|
|
|
|
|
+ projectID: number;
|
|
|
|
|
+ idx: number;
|
|
|
|
|
+ inst: any;
|
|
|
|
|
+ deleteIndex: (index: number) => void;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const SlackIntegration: React.FC<SlackIntegrationProps> = ({
|
|
|
|
|
+ projectID,
|
|
|
|
|
+ idx,
|
|
|
|
|
+ inst,
|
|
|
|
|
+ deleteIndex,
|
|
|
|
|
+}) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Dropdown
|
|
|
|
|
+ key={idx.toString()}
|
|
|
|
|
+ title={inst.team_name || inst.team_id}
|
|
|
|
|
+ tag={
|
|
|
|
|
+ <Tag
|
|
|
|
|
+ borderRadiusPixels={10}
|
|
|
|
|
+ backgroundColor={"#55555555"}
|
|
|
|
|
+ hoverColor={"#55555577"}
|
|
|
|
|
+ borderColor={"#88888888"}
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ window.open(inst.configuration_url, "_blank");
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Container row>
|
|
|
|
|
+ <Icon src={hash} height={"12px"} />
|
|
|
|
|
+ <Spacer x={0.2} inline />
|
|
|
|
|
+ <Text size={13} color="#eeeeeedd">
|
|
|
|
|
+ {inst.channel}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </Container>
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ }
|
|
|
|
|
+ iconURL={inst.team_icon_url}
|
|
|
|
|
+ deleteFunc={() => {
|
|
|
|
|
+ deleteIndex(idx);
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <SetupNotificationConfig
|
|
|
|
|
+ projectID={projectID}
|
|
|
|
|
+ slackIntegrationID={inst.id}
|
|
|
|
|
+ notificationConfigID={inst.notification_config_id}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Dropdown>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+type NotificationConfigContainerProps = {
|
|
|
|
|
+ projectID: number;
|
|
|
|
|
+ notificationConfigID: number;
|
|
|
|
|
+ slackIntegrationID: number;
|
|
|
|
|
+ existingConfig: NotificationConfigFormData;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const NotificationConfigContainer: React.FC<
|
|
|
|
|
+ NotificationConfigContainerProps
|
|
|
|
|
+> = ({
|
|
|
|
|
+ projectID,
|
|
|
|
|
+ notificationConfigID,
|
|
|
|
|
+ slackIntegrationID,
|
|
|
|
|
+ existingConfig,
|
|
|
|
|
+}) => {
|
|
|
|
|
+ const [isUpdating, setIsUpdating] = useState(false);
|
|
|
|
|
+ const [isSuccessful, setIsSuccessful] = useState(false);
|
|
|
|
|
+ const [updateError, setUpdateError] = useState("");
|
|
|
|
|
+
|
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
|
+
|
|
|
|
|
+ const notificationForm = useForm<NotificationConfigFormData>({
|
|
|
|
|
+ resolver: zodResolver(notificationConfigFormValidator),
|
|
|
|
|
+ reValidateMode: "onSubmit",
|
|
|
|
|
+ defaultValues: existingConfig,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ control,
|
|
|
|
|
+ formState: { isSubmitting: isValidating, errors },
|
|
|
|
|
+ register,
|
|
|
|
|
+ } = notificationForm;
|
|
|
|
|
+
|
|
|
|
|
+ const { append, remove, fields } = useFieldArray({
|
|
|
|
|
+ control,
|
|
|
|
|
+ name: "statuses",
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const onAdd = (
|
|
|
|
|
+ inp: IterableElement<NotificationConfigFormData["statuses"]>
|
|
|
|
|
+ ): void => {
|
|
|
|
|
+ const previouslyAdded = fields.findIndex((s) => s.status === inp.status);
|
|
|
|
|
+
|
|
|
|
|
+ if (previouslyAdded === -1) {
|
|
|
|
|
+ append(inp);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const submitBtnStatus = useMemo(() => {
|
|
|
|
|
+ if (isValidating || isUpdating) {
|
|
|
|
|
+ return "loading";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (updateError) {
|
|
|
|
|
+ return <Error message={updateError} />;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isSuccessful) {
|
|
|
|
|
+ return "success";
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [isValidating, isUpdating, updateError, errors, isSuccessful]);
|
|
|
|
|
+
|
|
|
|
|
+ const handleSubmit = notificationForm.handleSubmit(async (data) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setIsUpdating(true);
|
|
|
|
|
+
|
|
|
|
|
+ await api.updateNotificationConfig(
|
|
|
|
|
+ "<token>",
|
|
|
|
|
+ {
|
|
|
|
|
+ slack_integration_id: slackIntegrationID,
|
|
|
|
|
+ config: data,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ project_id: projectID,
|
|
|
|
|
+ notification_config_id: notificationConfigID,
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+ await queryClient.invalidateQueries({
|
|
|
|
|
+ queryKey: ["getNotificationConfig"],
|
|
|
|
|
+ });
|
|
|
|
|
+ setIsUpdating(false);
|
|
|
|
|
+ setIsSuccessful(true);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setIsUpdating(false);
|
|
|
|
|
+ if (axios.isAxiosError(err) && err.response?.data?.error) {
|
|
|
|
|
+ setUpdateError(err.response?.data?.error);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ setUpdateError(
|
|
|
|
|
+ "An error occurred while updating your notification config. Please try again."
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Text>Filter deployment notifications:</Text>
|
|
|
|
|
+ <Spacer y={0.5} />
|
|
|
|
|
+ <SelectableList
|
|
|
|
|
+ scroll={false}
|
|
|
|
|
+ listItems={statusOptions.map((option) => {
|
|
|
|
|
+ const selectedOptionsIdx = fields.findIndex(
|
|
|
|
|
+ (s) => s.status === option.value
|
|
|
|
|
+ );
|
|
|
|
|
+ return {
|
|
|
|
|
+ selectable: (
|
|
|
|
|
+ <Container row>
|
|
|
|
|
+ <Spacer inline width="1px" />
|
|
|
|
|
+ <Text size={12}>
|
|
|
|
|
+ {option.emoji}
|
|
|
|
|
+ <Spacer inline x={0.7} />
|
|
|
|
|
+ {option.label}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ <Spacer inline x={1} />
|
|
|
|
|
+ </Container>
|
|
|
|
|
+ ),
|
|
|
|
|
+ key: option.value,
|
|
|
|
|
+ onSelect: () => {
|
|
|
|
|
+ onAdd({ status: option.value });
|
|
|
|
|
+ },
|
|
|
|
|
+ onDeselect: () => {
|
|
|
|
|
+ remove(selectedOptionsIdx);
|
|
|
|
|
+ },
|
|
|
|
|
+ isSelected: selectedOptionsIdx !== -1,
|
|
|
|
|
+ };
|
|
|
|
|
+ })}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Spacer y={0.75} />
|
|
|
|
|
+ <Text>@ Mention (only on failure):</Text>
|
|
|
|
|
+ <Spacer y={0.5} />
|
|
|
|
|
+ <ControlledInput
|
|
|
|
|
+ placeholder="ex: oncall"
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ width="300px"
|
|
|
|
|
+ error={errors.mention?.message}
|
|
|
|
|
+ {...register("mention")}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Spacer y={0.75} />
|
|
|
|
|
+ <form onSubmit={handleSubmit}>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ status={submitBtnStatus}
|
|
|
|
|
+ loadingText={"Saving..."}
|
|
|
|
|
+ onClick={handleSubmit}
|
|
|
|
|
+ successText={"Saved!"}
|
|
|
|
|
+ disabled={isUpdating}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Icon src={save} height={"13px"} />
|
|
|
|
|
+ <Spacer inline x={0.5} />
|
|
|
|
|
+ Save
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+type SetupNotificationConfigProps = {
|
|
|
|
|
+ projectID: number;
|
|
|
|
|
+ notificationConfigID: number;
|
|
|
|
|
+ slackIntegrationID: number;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const SetupNotificationConfig: React.FC<SetupNotificationConfigProps> = ({
|
|
|
|
|
+ projectID,
|
|
|
|
|
+ notificationConfigID,
|
|
|
|
|
+ slackIntegrationID,
|
|
|
|
|
+}) => {
|
|
|
|
|
+ const configRes = useQuery(
|
|
|
|
|
+ ["getNotificationConfig", projectID, notificationConfigID],
|
|
|
|
|
+ async () => {
|
|
|
|
|
+ if (notificationConfigID === 0) {
|
|
|
|
|
+ return emptyNotificationConfig;
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = await api.getNotificationConfig(
|
|
|
|
|
+ "<token>",
|
|
|
|
|
+ {},
|
|
|
|
|
+ {
|
|
|
|
|
+ project_id: projectID,
|
|
|
|
|
+ notification_config_id: notificationConfigID,
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const object = await z
|
|
|
|
|
+ .object({
|
|
|
|
|
+ config: notificationConfigFormValidator,
|
|
|
|
|
+ })
|
|
|
|
|
+ .parseAsync(res.data);
|
|
|
|
|
+
|
|
|
|
|
+ return object.config;
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {match(configRes)
|
|
|
|
|
+ .with({ status: "loading" }, () => <Loading />)
|
|
|
|
|
+ .with({ status: "success" }, ({ data }) => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <NotificationConfigContainer
|
|
|
|
|
+ projectID={projectID}
|
|
|
|
|
+ slackIntegrationID={slackIntegrationID}
|
|
|
|
|
+ notificationConfigID={notificationConfigID}
|
|
|
|
|
+ existingConfig={data}
|
|
|
|
|
+ />
|
|
|
|
|
+ );
|
|
|
|
|
+ })
|
|
|
|
|
+ .otherwise(() => null)}
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const Placeholder = styled.div`
|
|
const Placeholder = styled.div`
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 250px;
|
|
height: 250px;
|
|
@@ -111,96 +384,6 @@ const Placeholder = styled.div`
|
|
|
border: 1px solid #494b4f;
|
|
border: 1px solid #494b4f;
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
-const Label = styled.div`
|
|
|
|
|
- color: #ffffff;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
-`;
|
|
|
|
|
-
|
|
|
|
|
const StyledIntegrationList = styled.div`
|
|
const StyledIntegrationList = styled.div`
|
|
|
margin-bottom: 80px;
|
|
margin-bottom: 80px;
|
|
|
`;
|
|
`;
|
|
|
-
|
|
|
|
|
-const MainRow = styled.div`
|
|
|
|
|
- height: 70px;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- padding: 25px;
|
|
|
|
|
- border-radius: 5px;
|
|
|
|
|
- :hover {
|
|
|
|
|
- background: ${(props: { disabled: boolean }) =>
|
|
|
|
|
- props.disabled ? "" : "#ffffff11"};
|
|
|
|
|
- > i {
|
|
|
|
|
- background: ${(props: { disabled: boolean }) =>
|
|
|
|
|
- props.disabled ? "" : "#ffffff11"};
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- > i {
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- padding: 5px;
|
|
|
|
|
- color: #ffffff44;
|
|
|
|
|
- margin-right: -7px;
|
|
|
|
|
- :hover {
|
|
|
|
|
- background: ${(props: { disabled: boolean }) =>
|
|
|
|
|
- props.disabled ? "" : "#ffffff11"};
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-`;
|
|
|
|
|
-
|
|
|
|
|
-const Integration = styled.div`
|
|
|
|
|
- margin-left: -2px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- background: #26282f;
|
|
|
|
|
- cursor: ${(props: { disabled: boolean }) =>
|
|
|
|
|
- props.disabled ? "not-allowed" : "pointer"};
|
|
|
|
|
- margin-bottom: 15px;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- box-shadow: 0 4px 15px 0px #00000055;
|
|
|
|
|
-`;
|
|
|
|
|
-
|
|
|
|
|
-const Icon = styled.img`
|
|
|
|
|
- width: 27px;
|
|
|
|
|
- margin-right: 12px;
|
|
|
|
|
- margin-bottom: -1px;
|
|
|
|
|
-`;
|
|
|
|
|
-
|
|
|
|
|
-const Flex = styled.div`
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
-
|
|
|
|
|
- > i {
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- font-size: 24px;
|
|
|
|
|
- color: #969fbbaa;
|
|
|
|
|
- padding: 3px;
|
|
|
|
|
- margin-right: 11px;
|
|
|
|
|
- border-radius: 100px;
|
|
|
|
|
- :hover {
|
|
|
|
|
- background: #ffffff11;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-`;
|
|
|
|
|
-
|
|
|
|
|
-const MaterialIconTray = styled.div`
|
|
|
|
|
- max-width: 60px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- > i {
|
|
|
|
|
- background: #26282f;
|
|
|
|
|
- border-radius: 20px;
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- padding: 5px;
|
|
|
|
|
- margin: 0 5px;
|
|
|
|
|
- color: #ffffff44;
|
|
|
|
|
- :hover {
|
|
|
|
|
- background: ${(props: { disabled: boolean }) =>
|
|
|
|
|
- props.disabled ? "" : "#ffffff11"};
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-`;
|
|
|