visuddhinanda 2 yıl önce
ebeveyn
işleme
683da4ba82

+ 42 - 0
dashboard/src/components/api/webhook.ts

@@ -0,0 +1,42 @@
+import { IUser } from "../auth/User";
+import { TResType } from "../discussion/DiscussionListCard";
+
+export type TReceiverType = "wechat" | "dingtalk";
+
+export interface IWebhookRequest {
+  res_type: TResType;
+  res_id: string;
+  url: string;
+  receiver: TReceiverType;
+  event?: string[] | null;
+  status?: string;
+}
+
+export interface IWebhookApiData {
+  id: string;
+  res_type: TResType;
+  res_id: string;
+  url: string;
+  receiver: TReceiverType;
+  event: string[] | null;
+  fail: number;
+  success: number;
+  status: string;
+  editor: IUser;
+  created_at: string | null;
+  updated_at: string | null;
+}
+
+export interface IWebhookResponse {
+  ok: boolean;
+  message: string;
+  data: IWebhookApiData;
+}
+export interface IWebhookListResponse {
+  ok: boolean;
+  message: string;
+  data: {
+    rows: IWebhookApiData[];
+    count: number;
+  };
+}

+ 84 - 0
dashboard/src/components/channel/Edit.tsx

@@ -0,0 +1,84 @@
+import { useIntl } from "react-intl";
+import {
+  ProForm,
+  ProFormText,
+  ProFormTextArea,
+} from "@ant-design/pro-components";
+import { message } from "antd";
+
+import { IApiResponseChannel } from "../../components/api/Channel";
+import { get, put } from "../../request";
+import ChannelTypeSelect from "../../components/channel/ChannelTypeSelect";
+import LangSelect from "../../components/general/LangSelect";
+import PublicitySelect from "../../components/studio/PublicitySelect";
+
+interface IFormData {
+  name: string;
+  type: string;
+  lang: string;
+  summary: string;
+  status: number;
+  studio: string;
+}
+interface IWidget {
+  studioName?: string;
+  channelId?: string;
+  onLoad?: Function;
+}
+const EditWidget = ({ studioName, channelId, onLoad }: IWidget) => {
+  const intl = useIntl();
+
+  return (
+    <ProForm<IFormData>
+      onFinish={async (values: IFormData) => {
+        console.log(values);
+        const res = await put(`/v2/channel/${channelId}`, values);
+        console.log(res);
+        message.success(intl.formatMessage({ id: "flashes.success" }));
+      }}
+      formKey="channel_edit"
+      request={async () => {
+        const res = await get<IApiResponseChannel>(`/v2/channel/${channelId}`);
+        if (typeof onLoad !== "undefined") {
+          onLoad(res.data);
+        }
+        return {
+          name: res.data.name,
+          type: res.data.type,
+          lang: res.data.lang,
+          summary: res.data.summary,
+          status: res.data.status,
+          studio: studioName ? studioName : "",
+        };
+      }}
+    >
+      <ProForm.Group>
+        <ProFormText
+          width="md"
+          name="name"
+          required
+          label={intl.formatMessage({ id: "channel.name" })}
+          rules={[
+            {
+              required: true,
+            },
+          ]}
+        />
+      </ProForm.Group>
+
+      <ProForm.Group>
+        <ChannelTypeSelect />
+        <LangSelect />
+      </ProForm.Group>
+      <ProForm.Group>
+        <PublicitySelect />
+      </ProForm.Group>
+
+      <ProForm.Group>
+        <ProFormTextArea width="md" name="summary" label="简介" />
+      </ProForm.Group>
+    </ProForm>
+  );
+};
+
+export default EditWidget;

+ 160 - 0
dashboard/src/components/webhook/WebhookEdit.tsx

@@ -0,0 +1,160 @@
+import {
+  ProForm,
+  ProFormInstance,
+  ProFormSelect,
+  ProFormText,
+} from "@ant-design/pro-components";
+import { message, Space, Typography } from "antd";
+import { useRef } from "react";
+import { useIntl } from "react-intl";
+import { Link } from "react-router-dom";
+
+import { get, post, put } from "../../request";
+import { IWebhookRequest, IWebhookResponse } from "../api/webhook";
+import { TResType } from "../discussion/DiscussionListCard";
+
+const { Title } = Typography;
+
+interface IWidget {
+  studioName?: string;
+  channelId?: string;
+  id?: string;
+  res_type?: TResType;
+  res_id?: string;
+  onSuccess?: Function;
+}
+const WebhookEditWidget = ({
+  studioName,
+  channelId,
+  id,
+  res_type = "channel",
+  res_id = "",
+  onSuccess,
+}: IWidget) => {
+  const formRef = useRef<ProFormInstance>();
+  const intl = useIntl();
+
+  return (
+    <Space direction="vertical">
+      <Title level={4}>
+        <Link
+          to={`/studio/${studioName}/channel/${channelId}/setting/webhooks`}
+        >
+          List
+        </Link>{" "}
+        / {id ? "Manage webhook" : "New"}
+      </Title>
+      <ProForm<IWebhookRequest>
+        formRef={formRef}
+        autoFocusFirstInput
+        onFinish={async (values) => {
+          console.log("submit", values);
+          let data: IWebhookRequest = values;
+          data.res_id = res_id;
+          data.res_type = res_type;
+          let res: IWebhookResponse;
+          if (typeof id === "undefined") {
+            res = await post<IWebhookRequest, IWebhookResponse>(
+              `/v2/webhook`,
+              data
+            );
+          } else {
+            res = await put<IWebhookRequest, IWebhookResponse>(
+              `/v2/webhook/${id}`,
+              data
+            );
+          }
+          console.log(res);
+          if (res.ok) {
+            message.success("提交成功");
+            if (typeof onSuccess !== "undefined") {
+              onSuccess();
+            }
+          } else {
+            message.error(res.message);
+          }
+
+          return true;
+        }}
+        request={
+          id
+            ? async () => {
+                const url = `/v2/webhook/${id}`;
+                const res: IWebhookResponse = await get<IWebhookResponse>(url);
+                console.log("get", res);
+                if (res.ok) {
+                  return res.data;
+                } else {
+                  return {
+                    res_type: res_type,
+                    res_id: res_id,
+                    url: "",
+                    receiver: "wechat",
+                    event: [],
+                    status: "normal",
+                  };
+                }
+              }
+            : undefined
+        }
+      >
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            required
+            name="url"
+            label={intl.formatMessage({ id: "forms.fields.url.label" })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormSelect
+            options={[
+              { value: "wechat", label: "wechat" },
+              { value: "dingtalk", label: "dingtalk" },
+            ]}
+            width="md"
+            required
+            name={"receiver"}
+            allowClear={false}
+            label={intl.formatMessage({ id: "forms.fields.receiver.label" })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormSelect
+            placeholder={"全部事件"}
+            options={["pr", "discussion", "content"].map((item) => {
+              return {
+                value: item,
+                label: item,
+              };
+            })}
+            fieldProps={{
+              mode: "tags",
+            }}
+            width="md"
+            name="event"
+            allowClear={false}
+            label={intl.formatMessage({ id: "forms.fields.event.label" })}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormSelect
+            placeholder={"active"}
+            options={["active", "disable"].map((item) => {
+              return {
+                value: item,
+                label: item,
+              };
+            })}
+            width="md"
+            name="status"
+            allowClear={false}
+            label={intl.formatMessage({ id: "forms.fields.status.label" })}
+          />
+        </ProForm.Group>
+      </ProForm>
+    </Space>
+  );
+};
+
+export default WebhookEditWidget;

+ 236 - 0
dashboard/src/components/webhook/WebhookList.tsx

@@ -0,0 +1,236 @@
+import { ActionType, ProTable } from "@ant-design/pro-components";
+import { useIntl } from "react-intl";
+import { Link, useNavigate } from "react-router-dom";
+import { message, Modal, Space, Typography } from "antd";
+import { Button, Dropdown } from "antd";
+import {
+  PlusOutlined,
+  ExclamationCircleOutlined,
+  DeleteOutlined,
+  StopOutlined,
+  CheckCircleOutlined,
+  WarningOutlined,
+} from "@ant-design/icons";
+
+import { delete_, get } from "../../request";
+import { IDeleteResponse } from "../api/Article";
+import { useRef } from "react";
+import { IWebhookApiData, IWebhookListResponse } from "../api/webhook";
+
+const { Text } = Typography;
+
+interface IWidget {
+  channelId?: string;
+  studioName?: string;
+}
+
+const WebhookListWidget = ({ channelId, studioName }: IWidget) => {
+  const intl = useIntl();
+  const navigate = useNavigate();
+
+  const showDeleteConfirm = (id: string, title: string) => {
+    Modal.confirm({
+      icon: <ExclamationCircleOutlined />,
+      title:
+        intl.formatMessage({
+          id: "message.delete.sure",
+        }) +
+        intl.formatMessage({
+          id: "message.irrevocable",
+        }),
+
+      content: title,
+      okText: intl.formatMessage({
+        id: "buttons.delete",
+      }),
+      okType: "danger",
+      cancelText: intl.formatMessage({
+        id: "buttons.no",
+      }),
+      onOk() {
+        console.log("delete", id);
+        return delete_<IDeleteResponse>(`/v2/webhook/${id}`)
+          .then((json) => {
+            if (json.ok) {
+              message.success("删除成功");
+              ref.current?.reload();
+            } else {
+              message.error(json.message);
+            }
+          })
+          .catch((e) => console.log("Oops errors!", e));
+      },
+    });
+  };
+
+  const ref = useRef<ActionType>();
+
+  return (
+    <>
+      <ProTable<IWebhookApiData>
+        actionRef={ref}
+        columns={[
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.url.label",
+            }),
+            dataIndex: "url",
+            key: "url",
+            tip: "过长会自动收缩",
+            ellipsis: true,
+            render: (text, row, index, action) => {
+              const url = row.url.split("?")[0];
+              return (
+                <Space>
+                  {row.status === "disable" ? (
+                    <StopOutlined style={{ color: "red" }} />
+                  ) : (
+                    <CheckCircleOutlined style={{ color: "green" }} />
+                  )}
+                  {url}
+                  <Text type="secondary" italic>
+                    <Space>{row.event}</Space>
+                  </Text>
+                </Space>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.receiver.label",
+            }),
+            dataIndex: "receiver",
+            key: "receiver",
+            width: 100,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.fail.label",
+            }),
+            dataIndex: "fail",
+            key: "fail",
+            width: 100,
+            search: false,
+            render: (text, row, index, action) => {
+              return (
+                <Space>
+                  {row.fail > 0 ? (
+                    <WarningOutlined style={{ color: "orange" }} />
+                  ) : undefined}
+                  {row.fail}
+                </Space>
+              );
+            },
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.success.label",
+            }),
+            dataIndex: "success",
+            key: "success",
+            width: 100,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({
+              id: "forms.fields.status.label",
+            }),
+            dataIndex: "status",
+            key: "status",
+            width: 100,
+            search: false,
+          },
+          {
+            title: intl.formatMessage({ id: "buttons.option" }),
+            key: "option",
+            width: 120,
+            valueType: "option",
+            render: (text, row, index, action) => {
+              return [
+                <Dropdown.Button
+                  key={index}
+                  type="link"
+                  trigger={["click", "contextMenu"]}
+                  menu={{
+                    items: [
+                      {
+                        key: "remove",
+                        label: intl.formatMessage({
+                          id: "buttons.delete",
+                        }),
+                        icon: <DeleteOutlined />,
+                        danger: true,
+                      },
+                    ],
+                    onClick: (e) => {
+                      switch (e.key) {
+                        case "remove":
+                          showDeleteConfirm(row.id, row.url);
+                          break;
+                        default:
+                          break;
+                      }
+                    },
+                  }}
+                >
+                  <Link
+                    to={`/studio/${studioName}/channel/${channelId}/setting/webhooks/${row.id}`}
+                  >
+                    {intl.formatMessage({
+                      id: "buttons.edit",
+                    })}
+                  </Link>
+                </Dropdown.Button>,
+              ];
+            },
+          },
+        ]}
+        request={async (params = {}, sorter, filter) => {
+          console.log(params, sorter, filter);
+          let url = `/v2/webhook?view=channel&id=${channelId}`;
+          const offset =
+            ((params.current ? params.current : 1) - 1) *
+            (params.pageSize ? params.pageSize : 20);
+          url += `&limit=${params.pageSize}&offset=${offset}`;
+          url += params.keyword ? "&search=" + params.keyword : "";
+
+          console.log("url", url);
+          const res: IWebhookListResponse = await get(url);
+
+          return {
+            total: res.data.count,
+            succcess: true,
+            data: res.data.rows,
+          };
+        }}
+        rowKey="id"
+        bordered
+        pagination={{
+          showQuickJumper: true,
+          showSizeChanger: true,
+        }}
+        search={false}
+        options={{
+          search: true,
+        }}
+        toolBarRender={() => [
+          <Button
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+            onClick={() =>
+              navigate(
+                `/studio/${studioName}/channel/${channelId}/setting/webhooks/new`
+              )
+            }
+          >
+            {intl.formatMessage({ id: "buttons.create" })}
+          </Button>,
+        ]}
+      />
+    </>
+  );
+};
+
+export default WebhookListWidget;

+ 93 - 0
dashboard/src/pages/studio/channel/setting.tsx

@@ -0,0 +1,93 @@
+import { useState } from "react";
+import { useIntl } from "react-intl";
+import { useNavigate, useParams } from "react-router-dom";
+import { TeamOutlined } from "@ant-design/icons";
+import { Button, Card, Tabs } from "antd";
+
+import { IApiResponseChannelData } from "../../../components/api/Channel";
+import GoBack from "../../../components/studio/GoBack";
+import ShareModal from "../../../components/share/ShareModal";
+import { EResType } from "../../../components/share/Share";
+import Edit from "../../../components/channel/Edit";
+import WebhookList from "../../../components/webhook/WebhookList";
+import WebhookEdit from "../../../components/webhook/WebhookEdit";
+
+const Widget = () => {
+  const intl = useIntl();
+  const { studioname } = useParams();
+  const { channelId } = useParams(); //url 参数
+  const { type } = useParams();
+  const { id } = useParams();
+  const [title, setTitle] = useState("");
+  const navigate = useNavigate();
+
+  return (
+    <Card
+      title={<GoBack to={`/studio/${studioname}/channel/list`} title={title} />}
+      extra={
+        channelId ? (
+          <ShareModal
+            trigger={
+              <Button icon={<TeamOutlined />}>
+                {intl.formatMessage({
+                  id: "buttons.share",
+                })}
+              </Button>
+            }
+            resId={channelId}
+            resType={EResType.channel}
+          />
+        ) : undefined
+      }
+    >
+      <Tabs
+        size="small"
+        defaultActiveKey={type}
+        onChange={(activeKey: string) => {
+          navigate(
+            `/studio/${studioname}/channel/${channelId}/setting/${activeKey}`
+          );
+        }}
+        items={[
+          {
+            label: `基本信息`,
+            key: "basic",
+            children: (
+              <Edit
+                studioName={studioname}
+                channelId={channelId}
+                onLoad={(data: IApiResponseChannelData) => setTitle(data.name)}
+              />
+            ),
+          },
+          {
+            label: `Webhooks`,
+            key: "webhooks",
+            children: id ? (
+              id === "new" ? (
+                <WebhookEdit
+                  studioName={studioname}
+                  channelId={channelId}
+                  res_type="channel"
+                  res_id={channelId}
+                />
+              ) : (
+                <WebhookEdit
+                  studioName={studioname}
+                  channelId={channelId}
+                  id={id}
+                  res_type="channel"
+                  res_id={channelId}
+                />
+              )
+            ) : (
+              <WebhookList studioName={studioname} channelId={channelId} />
+            ),
+          },
+        ]}
+      />
+    </Card>
+  );
+};
+
+export default Widget;