@ant-design/icons#ClockCircleOutlined TypeScript Examples

The following examples show how to use @ant-design/icons#ClockCircleOutlined. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: general-time-picker.editor.spec.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
describe("GeneralTimePickerEditor", () => {
  it("should work", () => {
    mockUseBuilderNode.mockReturnValueOnce({
      type: "brick",
      id: "B-001",
      brick: "general-time-picker",
      alias: "my-brick",
      $$parsedProperties: {
        placeholder: "请选择时间",
      },
    });
    const wrapper = shallow(<GeneralTimePickerEditor nodeUid={1} />);
    expect(wrapper.find(".placeholder").text()).toBe("请选择时间");
    expect(wrapper.find(ClockCircleOutlined).length).toEqual(1);
  });
});
Example #2
Source File: time-range-picker.editor.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
export function TimeRangePickerEditor({
  nodeUid,
}: EditorComponentProps): React.ReactElement {
  const node = useBuilderNode<TimeRangePickerProperties>({ nodeUid });
  const { required, label } = node.$$parsedProperties;
  return (
    <EditorContainer nodeUid={nodeUid}>
      <div className={formSharedStyle.formItemWrapper}>
        <div
          className={classNames(formSharedStyle.labelContainer, {
            [formSharedStyle.requireMark]: required,
          })}
        >
          <span
            className={classNames({ [formSharedStyle.formLabel]: !!label })}
          >
            {formCommonFieldDisplay(label)}
          </span>
        </div>
        <div className={styles.rangePickerItem}>
          <div className={styles.leftPicker}>
            <ClockCircleOutlined />
          </div>
          <span className={styles.union}></span>
          <div className={styles.rightPicker}>
            <ClockCircleOutlined />
          </div>
        </div>
      </div>
    </EditorContainer>
  );
}
Example #3
Source File: StatusIcon.tsx    From datart with Apache License 2.0 6 votes vote down vote up
WaitingIcon: React.FC<{
  title: React.ReactNode;
  onClick?: React.MouseEventHandler<HTMLSpanElement> | undefined;
  onMouseEnter?: React.MouseEventHandler<HTMLSpanElement> | undefined;
}> = ({ title, onClick, onMouseEnter }) => {
  return (
    <Tooltip title={title ?? 'waitingLoad'}>
      <Button
        onClick={onClick}
        onMouseEnter={onMouseEnter}
        icon={<ClockCircleOutlined style={{ color: PRIMARY }} />}
        type="link"
      />
    </Tooltip>
  );
}
Example #4
Source File: ToggleButtonChartFilter.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function ToggleButtonChartFilter({
    onChange = noop,
    disabled = false,
    simpleMode,
}: ToggleButtonChartFilterProps): JSX.Element | null {
    const { insightProps } = useValues(insightLogic)
    const { clickhouseFeaturesEnabled, aggregationTargetLabel } = useValues(funnelLogic(insightProps))
    const { chartFilter } = useValues(chartFilterLogic(insightProps))
    const { setChartFilter } = useActions(chartFilterLogic(insightProps))
    const defaultDisplay = FunnelVizType.Steps

    const options = [
        {
            key: FunnelVizType.Steps,
            label: 'Conversion steps',
            description: `Track ${aggregationTargetLabel.plural} progress between steps of the funnel`,
            icon: <FunnelPlotOutlined />,
        },
        {
            key: FunnelVizType.TimeToConvert,
            label: 'Time to convert',
            description: `Track how long it takes for ${aggregationTargetLabel.plural} to convert`,
            icon: <ClockCircleOutlined />,
            hidden: !clickhouseFeaturesEnabled,
        },
        {
            key: FunnelVizType.Trends,
            label: 'Historical trends',
            description: "Track how this funnel's conversion rate is trending over time",
            icon: <LineChartOutlined />,
            hidden: !clickhouseFeaturesEnabled,
        },
    ]

    if (options.filter((option) => !option.hidden).length <= 1) {
        return null
    }

    const innerContent = (
        <div className="funnel-chart-filter">
            <DropdownSelector
                options={options}
                value={chartFilter || defaultDisplay}
                onValueChange={(val) => {
                    const valueTyped = val as FunnelVizType
                    setChartFilter(valueTyped)
                    onChange(valueTyped)
                }}
                disabled={disabled}
                hideDescriptionOnDisplay
                compact={simpleMode}
            />
        </div>
    )

    return simpleMode ? (
        innerContent
    ) : (
        <div>
            <h4 className="secondary">Graph Type</h4>
            {innerContent}
        </div>
    )
}
Example #5
Source File: general-time-picker.editor.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
export function GeneralTimePickerEditor({
  nodeUid,
}: EditorComponentProps): React.ReactElement {
  const node = useBuilderNode<GeneralTimePickerProperties>({ nodeUid });
  const { label, required, placeholder } = node.$$parsedProperties;

  return (
    <EditorContainer nodeUid={nodeUid}>
      <div className={formSharedStyle.formItemWrapper}>
        <div
          className={classNames(formSharedStyle.labelContainer, {
            [formSharedStyle.requireMark]: required,
          })}
        >
          <span
            className={classNames({ [formSharedStyle.formLabel]: !!label })}
          >
            {formCommonFieldDisplay(label)}
          </span>
        </div>
        <div
          className={formSharedStyle.formInputItem}
          style={{ flex: "0 0 200px" }}
        >
          <div
            className={classNames(
              formSharedStyle.placeholder,
              formSharedStyle.placeholderOffset
            )}
          >
            <span>{formCommonFieldDisplay(placeholder)}</span>
          </div>
          <span className={styles.suffixIcon}>
            <ClockCircleOutlined />
          </span>
        </div>
      </div>
    </EditorContainer>
  );
}
Example #6
Source File: Paths.tsx    From posthog-foss with MIT License 4 votes vote down vote up
export function Paths({ dashboardItemId = null, color = 'white' }: PathsProps): JSX.Element {
    const canvas = useRef<HTMLDivElement>(null)
    const { width: canvasWidth = FALLBACK_CANVAS_WIDTH, height: canvasHeight = FALLBACK_CANVAS_HEIGHT } =
        useResizeObserver({ ref: canvas })
    const { insightProps } = useValues(insightLogic)
    const { paths, resultsLoading: pathsLoading, filter, pathsError } = useValues(pathsLogic(insightProps))
    const { openPersonsModal, setFilter, updateExclusions, viewPathToFunnel } = useActions(pathsLogic(insightProps))
    const [pathItemCards, setPathItemCards] = useState<PathNodeData[]>([])
    const { user } = useValues(userLogic)

    const hasAdvancedPaths = user?.organization?.available_features?.includes(AvailableFeature.PATHS_ADVANCED)

    useEffect(() => {
        setPathItemCards([])
        renderPaths()
    }, [paths, !pathsLoading, canvasWidth, canvasHeight, color])

    const createCanvas = (width: number, height: number): D3Selector => {
        return d3
            .select(canvas.current)
            .append('svg')
            .style('background', 'var(--item-background)')
            .style('width', width)
            .style('height', height)
    }

    const createSankey = (width: number, height: number): any => {
        // @ts-expect-error - d3 sankey typing things
        return new Sankey.sankey()
            .nodeId((d: PathNodeData) => d.name)
            .nodeAlign(Sankey.sankeyJustify)
            .nodeSort(null)
            .nodeWidth(15)
            .size([width, height])
    }

    const appendPathNodes = (svg: any, nodes: PathNodeData[]): void => {
        svg.append('g')
            .selectAll('rect')
            .data(nodes)
            .join('rect')
            .attr('x', (d: PathNodeData) => d.x0 + 1)
            .attr('y', (d: PathNodeData) => d.y0)
            .attr('height', (d: PathNodeData) => d.y1 - d.y0)
            .attr('width', (d: PathNodeData) => d.x1 - d.x0 - 2)
            .attr('fill', (d: PathNodeData) => {
                let c
                for (const link of d.sourceLinks) {
                    if (c === undefined) {
                        c = link.color
                    } else if (c !== link.color) {
                        c = null
                    }
                }
                if (c === undefined) {
                    for (const link of d.targetLinks) {
                        if (c === undefined) {
                            c = link.color
                        } else if (c !== link.color) {
                            c = null
                        }
                    }
                }
                if (isSelectedPathStartOrEnd(filter, d)) {
                    return d3.color('purple')
                }
                const startNodeColor =
                    c && d3.color(c) ? d3.color(c) : isMonochrome(color) ? d3.color('#5375ff') : d3.color('white')
                return startNodeColor
            })
            .on('mouseover', (data: PathNodeData) => {
                if (data.y1 - data.y0 > HIDE_PATH_CARD_HEIGHT) {
                    return
                }
                setPathItemCards(
                    nodes.map((node: PathNodeData) =>
                        node.index === data.index
                            ? { ...node, visible: true }
                            : { ...node, visible: node.y1 - node.y0 > HIDE_PATH_CARD_HEIGHT }
                    )
                )
            })
            .append('title')
            .text((d: PathNodeData) => `${stripHTTP(d.name)}\n${d.value.toLocaleString()}`)
    }

    const appendDropoffs = (svg: any): void => {
        const dropOffGradient = svg
            .append('defs')
            .append('linearGradient')
            .attr('id', 'dropoff-gradient')
            .attr('gradientTransform', 'rotate(90)')

        dropOffGradient
            .append('stop')
            .attr('offset', '0%')
            .attr('stop-color', color === 'white' ? 'rgba(220,53,69,0.7)' : 'rgb(220,53,69)')

        dropOffGradient
            .append('stop')
            .attr('offset', '100%')
            .attr('stop-color', color === 'white' ? '#fff' : 'var(--item-background)')
    }

    const appendPathLinks = (svg: any, links: PathNodeData[], nodes: PathNodeData[]): void => {
        const link = svg
            .append('g')
            .attr('fill', 'none')
            .selectAll('g')
            .data(links)
            .join('g')
            .attr('stroke', () => (isMonochrome(color) ? 'var(--primary)' : 'white'))
            .attr('opacity', 0.35)

        link.append('path')
            .attr('d', Sankey.sankeyLinkHorizontal())
            .attr('id', (d: PathNodeData) => `path-${d.index}`)
            .attr('stroke-width', (d: PathNodeData) => {
                return Math.max(1, d.width)
            })
            .on('mouseover', (data: PathNodeData) => {
                svg.select(`#path-${data.index}`).attr('stroke', 'blue')
                if (data?.source?.targetLinks.length === 0) {
                    return
                }
                const nodesToColor = [data.source]
                const pathCardsToShow: number[] = []
                while (nodesToColor.length > 0) {
                    const _node = nodesToColor.pop()
                    _node?.targetLinks.forEach((_link: PathTargetLink) => {
                        svg.select(`#path-${_link.index}`).attr('stroke', 'blue')
                        nodesToColor.push(_link.source)
                        pathCardsToShow.push(_link.source.index)
                    })
                }
                const pathCards = [data.target]
                pathCardsToShow.push(data.target.index, data.source.index)
                while (pathCards.length > 0) {
                    const node = pathCards.pop()
                    node?.sourceLinks.forEach((l: PathTargetLink) => {
                        pathCards.push(l.target)
                        pathCardsToShow.push(l.target.index)
                    })
                }
                setPathItemCards(
                    nodes.map((node: PathNodeData) => ({
                        ...node,
                        ...{
                            visible: pathCardsToShow.includes(node.index)
                                ? true
                                : node.y1 - node.y0 > HIDE_PATH_CARD_HEIGHT,
                        },
                    }))
                )
            })
            .on('mouseleave', () => {
                svg.selectAll('path').attr('stroke', () => (isMonochrome(color) ? 'var(--primary)' : 'white'))
            })

        link.append('g')
            .append('path')
            .attr('d', (data: PathNodeData) => {
                if (data.source.layer === 0) {
                    return
                }
                const _height =
                    data.source.y1 -
                    data.source.y0 -
                    data.source.sourceLinks.reduce((prev, curr) => prev + curr.width, 0)
                return roundedRect(0, 0, 30, _height, Math.min(25, _height), false, true, false, false)
            })
            .attr('fill', 'url(#dropoff-gradient)')
            .attr('stroke-width', 0)
            .attr('transform', (data: PathNodeData) => {
                return (
                    'translate(' +
                    Math.round(data.source.x1) +
                    ',' +
                    Math.round(data.source.y0 + data.source.sourceLinks.reduce((prev, curr) => prev + curr.width, 0)) +
                    ')'
                )
            })
    }

    const addChartAxisLines = (svg: any, height: number, nodes: PathNodeData[], maxLayer: number): void => {
        if (maxLayer > 5) {
            const arr = [...Array(maxLayer)]
            const minWidthApart = nodes[1].x0 - nodes[0].x0
            arr.forEach((_, i) => {
                svg.append('line')
                    .style('stroke', 'var(--border)')
                    .attr('stroke-width', 2)
                    .attr('x1', minWidthApart * (i + 1) - 20)
                    .attr('y1', 0)
                    .attr('x2', minWidthApart * (i + 1) - 20)
                    .attr('y2', height)
            })
        }
    }

    function renderPaths(): void {
        const elements = document
            ?.getElementById(`'${dashboardItemId || DEFAULT_PATHS_ID}'`)
            ?.querySelectorAll(`.paths svg`)
        elements?.forEach((node) => node?.parentNode?.removeChild(node))

        if (!paths || paths.nodes.length === 0) {
            setPathItemCards([])
            return
        }

        const maxLayer = paths.links.reduce((prev, curr) => {
            // @ts-expect-error - sometimes target is an object instead of string
            const currNum = curr.target.name || curr.target
            return Math.max(prev, Number(currNum.match(/[^_]*/)))
        }, 0)

        const minWidth = canvasWidth > FALLBACK_CANVAS_WIDTH || maxLayer < 3 ? canvasWidth : FALLBACK_CANVAS_WIDTH

        const width = maxLayer > 5 && canvasWidth ? (minWidth / 5) * maxLayer : minWidth
        const height = canvasHeight

        const svg = createCanvas(width, height)
        const sankey = createSankey(width, height)
        const { nodes, links } = sankey({
            nodes: paths.nodes.map((d) => ({ ...d })),
            links: paths.links.map((d) => ({ ...d })),
        })

        setPathItemCards(
            nodes.map((node: PathNodeData) => ({ ...node, visible: node.y1 - node.y0 > HIDE_PATH_CARD_HEIGHT }))
        )

        appendPathNodes(svg, nodes)
        appendDropoffs(svg)
        appendPathLinks(svg, links, nodes)
        addChartAxisLines(svg, height, nodes, maxLayer)
    }

    return (
        <div className="paths-container" id={`'${dashboardItemId || DEFAULT_PATHS_ID}'`}>
            <div ref={canvas} className="paths" data-attr="paths-viz">
                {!pathsLoading && paths && paths.nodes.length === 0 && !pathsError && <InsightEmptyState />}
                {!pathsError &&
                    pathItemCards &&
                    pathItemCards.map((pathItemCard: PathNodeData, idx) => {
                        const continuingValue = getContinuingValue(pathItemCard.sourceLinks)
                        const dropOffValue = getDropOffValue(pathItemCard)
                        return (
                            <Tooltip key={idx} title={pageUrl(pathItemCard)} placement="right">
                                <Dropdown
                                    key={idx}
                                    overlay={
                                        <Menu
                                            style={{
                                                marginTop: -5,
                                                border: '1px solid var(--border)',
                                                borderRadius: '0px 0px 4px 4px',
                                                width: 200,
                                            }}
                                        >
                                            {pathItemCard.sourceLinks.length > 0 && (
                                                <Menu.Item
                                                    disabled
                                                    className="pathcard-dropdown-info-option text-small"
                                                    style={{
                                                        borderBottom: `${
                                                            dropOffValue > 0 || pathItemCard.targetLinks.length > 0
                                                                ? '1px solid var(--border)'
                                                                : ''
                                                        }`,
                                                    }}
                                                >
                                                    <span className="text-small">
                                                        <span style={{ paddingRight: 8 }}>
                                                            <IconPathsCompletedArrow />
                                                        </span>{' '}
                                                        Continuing
                                                    </span>{' '}
                                                    <span className="primary text-small">
                                                        <ValueInspectorButton
                                                            style={{ paddingRight: 0, fontSize: 12 }}
                                                            onClick={() => openPersonsModal(pathItemCard.name)}
                                                        >
                                                            {continuingValue}
                                                            <span className="text-muted-alt" style={{ paddingLeft: 4 }}>
                                                                (
                                                                {((continuingValue / pathItemCard.value) * 100).toFixed(
                                                                    1
                                                                )}
                                                                %)
                                                            </span>
                                                        </ValueInspectorButton>
                                                    </span>
                                                </Menu.Item>
                                            )}
                                            {dropOffValue > 0 && (
                                                <Menu.Item
                                                    disabled
                                                    className="pathcard-dropdown-info-option text-small"
                                                    style={{
                                                        borderBottom: '1px solid var(--border)',
                                                    }}
                                                >
                                                    <span className="text-small" style={{ display: 'flex' }}>
                                                        <span
                                                            style={{
                                                                paddingRight: 8,
                                                                display: 'flex',
                                                                alignItems: 'center',
                                                            }}
                                                        >
                                                            <IconPathsDropoffArrow />
                                                        </span>{' '}
                                                        Dropping off
                                                    </span>{' '}
                                                    <span className="primary">
                                                        <ValueInspectorButton
                                                            style={{ paddingRight: 0, fontSize: 12 }}
                                                            onClick={() =>
                                                                openPersonsModal(
                                                                    undefined,
                                                                    undefined,
                                                                    pathItemCard.name
                                                                )
                                                            }
                                                        >
                                                            {dropOffValue}{' '}
                                                            <span
                                                                className="text-muted-alt text-small"
                                                                style={{ paddingLeft: 4 }}
                                                            >
                                                                (
                                                                {((dropOffValue / pathItemCard.value) * 100).toFixed(1)}
                                                                %)
                                                            </span>
                                                        </ValueInspectorButton>
                                                    </span>
                                                </Menu.Item>
                                            )}
                                            {pathItemCard.targetLinks.length > 0 && (
                                                <Menu.Item
                                                    disabled
                                                    className="pathcard-dropdown-info-option"
                                                    style={{
                                                        padding: '5px 8px',
                                                        fontWeight: 500,
                                                        fontSize: 12,
                                                    }}
                                                >
                                                    <ClockCircleOutlined
                                                        style={{ color: 'var(--muted)', fontSize: 16 }}
                                                    />
                                                    <span
                                                        className="text-small"
                                                        style={{
                                                            wordWrap: 'break-word',
                                                            whiteSpace: 'normal',
                                                            paddingLeft: 8,
                                                        }}
                                                    >
                                                        Average time from previous step{' '}
                                                    </span>
                                                    {humanFriendlyDuration(
                                                        pathItemCard.targetLinks[0].average_conversion_time / 1000
                                                    )}
                                                </Menu.Item>
                                            )}
                                        </Menu>
                                    }
                                    placement="bottomCenter"
                                >
                                    <Button
                                        key={idx}
                                        style={{
                                            position: 'absolute',
                                            left:
                                                pathItemCard.sourceLinks.length === 0
                                                    ? pathItemCard.x0 - (200 - 7)
                                                    : pathItemCard.x0 + 7,
                                            top:
                                                pathItemCard.sourceLinks.length > 0
                                                    ? pathItemCard.y0 + 5
                                                    : pathItemCard.y0 + (pathItemCard.y1 - pathItemCard.y0) / 2,
                                            background: 'white',
                                            width: 200,
                                            border: `1px solid ${
                                                isSelectedPathStartOrEnd(filter, pathItemCard)
                                                    ? 'purple'
                                                    : 'var(--border)'
                                            }`,
                                            padding: 4,
                                            justifyContent: 'space-between',
                                            alignItems: 'center',
                                            display: `${pathItemCard.visible ? 'flex' : 'none'}`,
                                        }}
                                    >
                                        <div style={{ display: 'flex', alignItems: 'center' }}>
                                            <span
                                                className="text-muted"
                                                style={{
                                                    fontSize: 10,
                                                    fontWeight: 600,
                                                    marginRight: 4,
                                                    lineHeight: '10px',
                                                }}
                                            >{`0${pathItemCard.name[0]}`}</span>{' '}
                                            <span style={{ fontSize: 12, fontWeight: 600 }}>
                                                {pageUrl(pathItemCard, true)}
                                            </span>
                                        </div>
                                        <Row style={{ alignSelf: 'center' }}>
                                            <span
                                                onClick={() => openPersonsModal(undefined, pathItemCard.name)}
                                                className="primary text-small"
                                                style={{ alignSelf: 'center', paddingRight: 4, fontWeight: 500 }}
                                            >
                                                {continuingValue + dropOffValue}
                                            </span>
                                            <Dropdown
                                                trigger={['click']}
                                                overlay={
                                                    <Menu className="paths-options-dropdown">
                                                        <Menu.Item
                                                            onClick={() =>
                                                                setFilter({ start_point: pageUrl(pathItemCard) })
                                                            }
                                                        >
                                                            Set as path start
                                                        </Menu.Item>
                                                        {hasAdvancedPaths && (
                                                            <>
                                                                <Menu.Item
                                                                    onClick={() =>
                                                                        setFilter({ end_point: pageUrl(pathItemCard) })
                                                                    }
                                                                >
                                                                    Set as path end
                                                                </Menu.Item>
                                                                <Menu.Item
                                                                    onClick={() => {
                                                                        updateExclusions([
                                                                            ...(filter.exclude_events || []),
                                                                            pageUrl(pathItemCard, false),
                                                                        ])
                                                                    }}
                                                                >
                                                                    Exclude path item
                                                                </Menu.Item>

                                                                <Menu.Item
                                                                    onClick={() => viewPathToFunnel(pathItemCard)}
                                                                >
                                                                    View funnel
                                                                </Menu.Item>
                                                            </>
                                                        )}
                                                        <Menu.Item
                                                            onClick={() => copyToClipboard(pageUrl(pathItemCard))}
                                                        >
                                                            Copy path item name
                                                        </Menu.Item>
                                                    </Menu>
                                                }
                                            >
                                                <div className="paths-dropdown-ellipsis">...</div>
                                            </Dropdown>
                                        </Row>
                                    </Button>
                                </Dropdown>
                            </Tooltip>
                        )
                    })}
            </div>
        </div>
    )
}
Example #7
Source File: Icon.tsx    From html2sketch with MIT License 4 votes vote down vote up
IconSymbol: FC = () => {
  return (
    <Row>
      {/*<CaretUpOutlined*/}
      {/*  className="icon"*/}
      {/*  symbolName={'1.General/2.Icons/1.CaretUpOutlined'}*/}
      {/*/>*/}
      {/*  className="icon"*/}
      {/*  symbolName={'1.General/2.Icons/2.MailOutlined'}*/}
      {/*/>*/}
      {/*<StepBackwardOutlined*/}
      {/*  className="icon"*/}
      {/*  symbolName={'1.General/2.Icons/2.StepBackwardOutlined'}*/}
      {/*/>*/}
      {/*<StepForwardOutlined*/}
      {/*  className="icon"*/}
      {/*  symbolName={'1.General/2.Icons/2.StepBackwardOutlined'}*/}
      {/*/>*/}
      <StepForwardOutlined />
      <ShrinkOutlined />
      <ArrowsAltOutlined />
      <DownOutlined />
      <UpOutlined />
      <LeftOutlined />
      <RightOutlined />
      <CaretUpOutlined />
      <CaretDownOutlined />
      <CaretLeftOutlined />
      <CaretRightOutlined />
      <VerticalAlignTopOutlined />
      <RollbackOutlined />
      <FastBackwardOutlined />
      <FastForwardOutlined />
      <DoubleRightOutlined />
      <DoubleLeftOutlined />
      <VerticalLeftOutlined />
      <VerticalRightOutlined />
      <VerticalAlignMiddleOutlined />
      <VerticalAlignBottomOutlined />
      <ForwardOutlined />
      <BackwardOutlined />
      <EnterOutlined />
      <RetweetOutlined />
      <SwapOutlined />
      <SwapLeftOutlined />
      <SwapRightOutlined />
      <ArrowUpOutlined />
      <ArrowDownOutlined />
      <ArrowLeftOutlined />
      <ArrowRightOutlined />
      <LoginOutlined />
      <LogoutOutlined />
      <MenuFoldOutlined />
      <MenuUnfoldOutlined />
      <BorderBottomOutlined />
      <BorderHorizontalOutlined />
      <BorderInnerOutlined />
      <BorderOuterOutlined />
      <BorderLeftOutlined />
      <BorderRightOutlined />
      <BorderTopOutlined />
      <BorderVerticleOutlined />
      <PicCenterOutlined />
      <PicLeftOutlined />
      <PicRightOutlined />
      <RadiusBottomleftOutlined />
      <RadiusBottomrightOutlined />
      <RadiusUpleftOutlined />
      <RadiusUprightOutlined />
      <FullscreenOutlined />
      <FullscreenExitOutlined />
      <QuestionOutlined />
      <PauseOutlined />
      <MinusOutlined />
      <PauseCircleOutlined />
      <InfoOutlined />
      <CloseOutlined />
      <ExclamationOutlined />
      <CheckOutlined />
      <WarningOutlined />
      <IssuesCloseOutlined />
      <StopOutlined />
      <EditOutlined />
      <CopyOutlined />
      <ScissorOutlined />
      <DeleteOutlined />
      <SnippetsOutlined />
      <DiffOutlined />
      <HighlightOutlined />
      <AlignCenterOutlined />
      <AlignLeftOutlined />
      <AlignRightOutlined />
      <BgColorsOutlined />
      <BoldOutlined />
      <ItalicOutlined />
      <UnderlineOutlined />
      <StrikethroughOutlined />
      <RedoOutlined />
      <UndoOutlined />
      <ZoomInOutlined />
      <ZoomOutOutlined />
      <FontColorsOutlined />
      <FontSizeOutlined />
      <LineHeightOutlined />
      <SortAscendingOutlined />
      <SortDescendingOutlined />
      <DragOutlined />
      <OrderedListOutlined />
      <UnorderedListOutlined />
      <RadiusSettingOutlined />
      <ColumnWidthOutlined />
      <ColumnHeightOutlined />
      <AreaChartOutlined />
      <PieChartOutlined />
      <BarChartOutlined />
      <DotChartOutlined />
      <LineChartOutlined />
      <RadarChartOutlined />
      <HeatMapOutlined />
      <FallOutlined />
      <RiseOutlined />
      <StockOutlined />
      <BoxPlotOutlined />
      <FundOutlined />
      <SlidersOutlined />
      <AndroidOutlined />
      <AppleOutlined />
      <WindowsOutlined />
      <IeOutlined />
      <ChromeOutlined />
      <GithubOutlined />
      <AliwangwangOutlined />
      <DingdingOutlined />
      <WeiboSquareOutlined />
      <WeiboCircleOutlined />
      <TaobaoCircleOutlined />
      <Html5Outlined />
      <WeiboOutlined />
      <TwitterOutlined />
      <WechatOutlined />
      <AlipayCircleOutlined />
      <TaobaoOutlined />
      <SkypeOutlined />
      <FacebookOutlined />
      <CodepenOutlined />
      <CodeSandboxOutlined />
      <AmazonOutlined />
      <GoogleOutlined />
      <AlipayOutlined />
      <AntDesignOutlined />
      <AntCloudOutlined />
      <ZhihuOutlined />
      <SlackOutlined />
      <SlackSquareOutlined />
      <BehanceSquareOutlined />
      <DribbbleOutlined />
      <DribbbleSquareOutlined />
      <InstagramOutlined />
      <YuqueOutlined />
      <AlibabaOutlined />
      <YahooOutlined />
      <RedditOutlined />
      <SketchOutlined />
      <AccountBookOutlined />
      <AlertOutlined />
      <ApartmentOutlined />
      <ApiOutlined />
      <QqOutlined />
      <MediumWorkmarkOutlined />
      <GitlabOutlined />
      <MediumOutlined />
      <GooglePlusOutlined />
      <AppstoreAddOutlined />
      <AppstoreOutlined />
      <AudioOutlined />
      <AudioMutedOutlined />
      <AuditOutlined />
      <BankOutlined />
      <BarcodeOutlined />
      <BarsOutlined />
      <BellOutlined />
      <BlockOutlined />
      <BookOutlined />
      <BorderOutlined />
      <BranchesOutlined />
      <BuildOutlined />
      <BulbOutlined />
      <CalculatorOutlined />
      <CalendarOutlined />
      <CameraOutlined />
      <CarOutlined />
      <CarryOutOutlined />
      <CiCircleOutlined />
      <CiOutlined />
      <CloudOutlined />
      <ClearOutlined />
      <ClusterOutlined />
      <CodeOutlined />
      <CoffeeOutlined />
      <CompassOutlined />
      <CompressOutlined />
      <ContactsOutlined />
      <ContainerOutlined />
      <ControlOutlined />
      <CopyrightCircleOutlined />
      <CopyrightOutlined />
      <CreditCardOutlined />
      <CrownOutlined />
      <CustomerServiceOutlined />
      <DashboardOutlined />
      <DatabaseOutlined />
      <DeleteColumnOutlined />
      <DeleteRowOutlined />
      <DisconnectOutlined />
      <DislikeOutlined />
      <DollarCircleOutlined />
      <DollarOutlined />
      <DownloadOutlined />
      <EllipsisOutlined />
      <EnvironmentOutlined />
      <EuroCircleOutlined />
      <EuroOutlined />
      <ExceptionOutlined />
      <ExpandAltOutlined />
      <ExpandOutlined />
      <ExperimentOutlined />
      <ExportOutlined />
      <EyeOutlined />
      <FieldBinaryOutlined />
      <FieldNumberOutlined />
      <FieldStringOutlined />
      <DesktopOutlined />
      <DingtalkOutlined />
      <FileAddOutlined />
      <FileDoneOutlined />
      <FileExcelOutlined />
      <FileExclamationOutlined />
      <FileOutlined />
      <FileImageOutlined />
      <FileJpgOutlined />
      <FileMarkdownOutlined />
      <FilePdfOutlined />
      <FilePptOutlined />
      <FileProtectOutlined />
      <FileSearchOutlined />
      <FileSyncOutlined />
      <FileTextOutlined />
      <FileUnknownOutlined />
      <FileWordOutlined />
      <FilterOutlined />
      <FireOutlined />
      <FlagOutlined />
      <FolderAddOutlined />
      <FolderOutlined />
      <FolderOpenOutlined />
      <ForkOutlined />
      <FormatPainterOutlined />
      <FrownOutlined />
      <FunctionOutlined />
      <FunnelPlotOutlined />
      <GatewayOutlined />
      <GifOutlined />
      <GiftOutlined />
      <GlobalOutlined />
      <GoldOutlined />
      <GroupOutlined />
      <HddOutlined />
      <HeartOutlined />
      <HistoryOutlined />
      <HomeOutlined />
      <HourglassOutlined />
      <IdcardOutlined />
      <ImportOutlined />
      <InboxOutlined />
      <InsertRowAboveOutlined />
      <InsertRowBelowOutlined />
      <InsertRowLeftOutlined />
      <InsertRowRightOutlined />
      <InsuranceOutlined />
      <InteractionOutlined />
      <KeyOutlined />
      <LaptopOutlined />
      <LayoutOutlined />
      <LikeOutlined />
      <LineOutlined />
      <LinkOutlined />
      <Loading3QuartersOutlined />
      <LoadingOutlined />
      <LockOutlined />
      <MailOutlined />
      <ManOutlined />
      <MedicineBoxOutlined />
      <MehOutlined />
      <MenuOutlined />
      <MergeCellsOutlined />
      <MessageOutlined />
      <MobileOutlined />
      <MoneyCollectOutlined />
      <MonitorOutlined />
      <MoreOutlined />
      <NodeCollapseOutlined />
      <NodeExpandOutlined />
      <NodeIndexOutlined />
      <NotificationOutlined />
      <NumberOutlined />
      <PaperClipOutlined />
      <PartitionOutlined />
      <PayCircleOutlined />
      <PercentageOutlined />
      <PhoneOutlined />
      <PictureOutlined />
      <PoundCircleOutlined />
      <PoundOutlined />
      <PoweroffOutlined />
      <PrinterOutlined />
      <ProfileOutlined />
      <ProjectOutlined />
      <PropertySafetyOutlined />
      <PullRequestOutlined />
      <PushpinOutlined />
      <QrcodeOutlined />
      <ReadOutlined />
      <ReconciliationOutlined />
      <RedEnvelopeOutlined />
      <ReloadOutlined />
      <RestOutlined />
      <RobotOutlined />
      <RocketOutlined />
      <SafetyCertificateOutlined />
      <SafetyOutlined />
      <ScanOutlined />
      <ScheduleOutlined />
      <SearchOutlined />
      <SecurityScanOutlined />
      <SelectOutlined />
      <SendOutlined />
      <SettingOutlined />
      <ShakeOutlined />
      <ShareAltOutlined />
      <ShopOutlined />
      <ShoppingCartOutlined />
      <ShoppingOutlined />
      <SisternodeOutlined />
      <SkinOutlined />
      <SmileOutlined />
      <SolutionOutlined />
      <SoundOutlined />
      <SplitCellsOutlined />
      <StarOutlined />
      <SubnodeOutlined />
      <SyncOutlined />
      <TableOutlined />
      <TabletOutlined />
      <TagOutlined />
      <TagsOutlined />
      <TeamOutlined />
      <ThunderboltOutlined />
      <ToTopOutlined />
      <ToolOutlined />
      <TrademarkCircleOutlined />
      <TrademarkOutlined />
      <TransactionOutlined />
      <TrophyOutlined />
      <UngroupOutlined />
      <UnlockOutlined />
      <UploadOutlined />
      <UsbOutlined />
      <UserAddOutlined />
      <UserDeleteOutlined />
      <UserOutlined />
      <UserSwitchOutlined />
      <UsergroupAddOutlined />
      <UsergroupDeleteOutlined />
      <VideoCameraOutlined />
      <WalletOutlined />
      <WifiOutlined />
      <BorderlessTableOutlined />
      <WomanOutlined />
      <BehanceOutlined />
      <DropboxOutlined />
      <DeploymentUnitOutlined />
      <UpCircleOutlined />
      <DownCircleOutlined />
      <LeftCircleOutlined />
      <RightCircleOutlined />
      <UpSquareOutlined />
      <DownSquareOutlined />
      <LeftSquareOutlined />
      <RightSquareOutlined />
      <PlayCircleOutlined />
      <QuestionCircleOutlined />
      <PlusCircleOutlined />
      <PlusSquareOutlined />
      <MinusSquareOutlined />
      <MinusCircleOutlined />
      <InfoCircleOutlined />
      <ExclamationCircleOutlined />
      <CloseCircleOutlined />
      <CloseSquareOutlined />
      <CheckCircleOutlined />
      <CheckSquareOutlined />
      <ClockCircleOutlined />
      <FormOutlined />
      <DashOutlined />
      <SmallDashOutlined />
      <YoutubeOutlined />
      <CodepenCircleOutlined />
      <AliyunOutlined />
      <PlusOutlined />
      <LinkedinOutlined />
      <AimOutlined />
      <BugOutlined />
      <CloudDownloadOutlined />
      <CloudServerOutlined />
      <CloudSyncOutlined />
      <CloudUploadOutlined />
      <CommentOutlined />
      <ConsoleSqlOutlined />
      <EyeInvisibleOutlined />
      <FileGifOutlined />
      <DeliveredProcedureOutlined />
      <FieldTimeOutlined />
      <FileZipOutlined />
      <FolderViewOutlined />
      <FundProjectionScreenOutlined />
      <FundViewOutlined />
      <MacCommandOutlined />
      <PlaySquareOutlined />
      <OneToOneOutlined />
      <RotateLeftOutlined />
      <RotateRightOutlined />
      <SaveOutlined />
      <SwitcherOutlined />
      <TranslationOutlined />
      <VerifiedOutlined />
      <VideoCameraAddOutlined />
      <WhatsAppOutlined />

      {/*</Col>*/}
    </Row>
  );
}
Example #8
Source File: index.tsx    From ql with MIT License 4 votes vote down vote up
Crontab = () => {
  const columns = [
    {
      title: '任务名',
      dataIndex: 'name',
      key: 'name',
      align: 'center' as const,
      render: (text: string, record: any) => (
        <span>{record.name || record._id}</span>
      ),
    },
    {
      title: '任务',
      dataIndex: 'command',
      key: 'command',
      width: '40%',
      align: 'center' as const,
      render: (text: string, record: any) => {
        return (
          <span
            style={{
              textAlign: 'left',
              width: '100%',
              display: 'inline-block',
              wordBreak: 'break-all',
            }}
          >
            {text}
          </span>
        );
      },
    },
    {
      title: '任务定时',
      dataIndex: 'schedule',
      key: 'schedule',
      align: 'center' as const,
    },
    {
      title: '状态',
      key: 'status',
      dataIndex: 'status',
      align: 'center' as const,
      render: (text: string, record: any) => (
        <>
          {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
            <>
              {record.status === CrontabStatus.idle && (
                <Tag icon={<ClockCircleOutlined />} color="default">
                  空闲中
                </Tag>
              )}
              {record.status === CrontabStatus.running && (
                <Tag
                  icon={<Loading3QuartersOutlined spin />}
                  color="processing"
                >
                  运行中
                </Tag>
              )}
              {record.status === CrontabStatus.queued && (
                <Tag icon={<FieldTimeOutlined />} color="default">
                  队列中
                </Tag>
              )}
            </>
          )}
          {record.isDisabled === 1 && record.status === CrontabStatus.idle && (
            <Tag icon={<CloseCircleOutlined />} color="error">
              已禁用
            </Tag>
          )}
        </>
      ),
    },
    {
      title: '操作',
      key: 'action',
      align: 'center' as const,
      render: (text: string, record: any, index: number) => (
        <Space size="middle">
          {record.status === CrontabStatus.idle && (
            <Tooltip title="运行">
              <a
                onClick={() => {
                  runCron(record, index);
                }}
              >
                <PlayCircleOutlined />
              </a>
            </Tooltip>
          )}
          {record.status !== CrontabStatus.idle && (
            <Tooltip title="停止">
              <a
                onClick={() => {
                  stopCron(record, index);
                }}
              >
                <PauseCircleOutlined />
              </a>
            </Tooltip>
          )}
          <Tooltip title="日志">
            <a
              onClick={() => {
                setLogCron({ ...record, timestamp: Date.now() });
              }}
            >
              <FileTextOutlined />
            </a>
          </Tooltip>
          <MoreBtn key="more" record={record} index={index} />
        </Space>
      ),
    },
  ];

  const [width, setWidth] = useState('100%');
  const [marginLeft, setMarginLeft] = useState(0);
  const [marginTop, setMarginTop] = useState(-72);
  const [value, setValue] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);
  const [isModalVisible, setIsModalVisible] = useState(false);
  const [editedCron, setEditedCron] = useState();
  const [searchText, setSearchText] = useState('');
  const [isLogModalVisible, setIsLogModalVisible] = useState(false);
  const [logCron, setLogCron] = useState<any>();
  const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [pageSize, setPageSize] = useState(20);

  const getCrons = () => {
    setLoading(true);
    request
      .get(`${config.apiPrefix}crons?searchValue=${searchText}`)
      .then((data: any) => {
        setValue(
          data.data.sort((a: any, b: any) => {
            const sortA = a.isDisabled ? 4 : a.status;
            const sortB = b.isDisabled ? 4 : b.status;
            return CrontabSort[sortA] - CrontabSort[sortB];
          }),
        );
      })
      .finally(() => setLoading(false));
  };

  const addCron = () => {
    setEditedCron(null as any);
    setIsModalVisible(true);
  };

  const editCron = (record: any, index: number) => {
    setEditedCron(record);
    setIsModalVisible(true);
  };

  const delCron = (record: any, index: number) => {
    Modal.confirm({
      title: '确认删除',
      content: (
        <>
          确认删除定时任务{' '}
          <Text style={{ wordBreak: 'break-all' }} type="warning">
            {record.name}
          </Text>{' '}
          吗
        </>
      ),
      onOk() {
        request
          .delete(`${config.apiPrefix}crons`, { data: [record._id] })
          .then((data: any) => {
            if (data.code === 200) {
              message.success('删除成功');
              const result = [...value];
              result.splice(index + pageSize * (currentPage - 1), 1);
              setValue(result);
            } else {
              message.error(data);
            }
          });
      },
      onCancel() {
        console.log('Cancel');
      },
    });
  };

  const runCron = (record: any, index: number) => {
    Modal.confirm({
      title: '确认运行',
      content: (
        <>
          确认运行定时任务{' '}
          <Text style={{ wordBreak: 'break-all' }} type="warning">
            {record.name}
          </Text>{' '}
          吗
        </>
      ),
      onOk() {
        request
          .put(`${config.apiPrefix}crons/run`, { data: [record._id] })
          .then((data: any) => {
            if (data.code === 200) {
              const result = [...value];
              result.splice(index + pageSize * (currentPage - 1), 1, {
                ...record,
                status: CrontabStatus.running,
              });
              setValue(result);
            } else {
              message.error(data);
            }
          });
      },
      onCancel() {
        console.log('Cancel');
      },
    });
  };

  const stopCron = (record: any, index: number) => {
    Modal.confirm({
      title: '确认停止',
      content: (
        <>
          确认停止定时任务{' '}
          <Text style={{ wordBreak: 'break-all' }} type="warning">
            {record.name}
          </Text>{' '}
          吗
        </>
      ),
      onOk() {
        request
          .put(`${config.apiPrefix}crons/stop`, { data: [record._id] })
          .then((data: any) => {
            if (data.code === 200) {
              const result = [...value];
              result.splice(index + pageSize * (currentPage - 1), 1, {
                ...record,
                pid: null,
                status: CrontabStatus.idle,
              });
              setValue(result);
            } else {
              message.error(data);
            }
          });
      },
      onCancel() {
        console.log('Cancel');
      },
    });
  };

  const enabledOrDisabledCron = (record: any, index: number) => {
    Modal.confirm({
      title: `确认${record.isDisabled === 1 ? '启用' : '禁用'}`,
      content: (
        <>
          确认{record.isDisabled === 1 ? '启用' : '禁用'}
          定时任务{' '}
          <Text style={{ wordBreak: 'break-all' }} type="warning">
            {record.name}
          </Text>{' '}
          吗
        </>
      ),
      onOk() {
        request
          .put(
            `${config.apiPrefix}crons/${
              record.isDisabled === 1 ? 'enable' : 'disable'
            }`,
            {
              data: [record._id],
            },
          )
          .then((data: any) => {
            if (data.code === 200) {
              const newStatus = record.isDisabled === 1 ? 0 : 1;
              const result = [...value];
              result.splice(index + pageSize * (currentPage - 1), 1, {
                ...record,
                isDisabled: newStatus,
              });
              setValue(result);
            } else {
              message.error(data);
            }
          });
      },
      onCancel() {
        console.log('Cancel');
      },
    });
  };

  const MoreBtn: React.FC<{
    record: any;
    index: number;
  }> = ({ record, index }) => (
    <Dropdown
      arrow
      trigger={['click']}
      overlay={
        <Menu onClick={({ key }) => action(key, record, index)}>
          <Menu.Item key="edit" icon={<EditOutlined />}>
            编辑
          </Menu.Item>
          <Menu.Item
            key="enableordisable"
            icon={
              record.isDisabled === 1 ? (
                <CheckCircleOutlined />
              ) : (
                <StopOutlined />
              )
            }
          >
            {record.isDisabled === 1 ? '启用' : '禁用'}
          </Menu.Item>
          {record.isSystem !== 1 && (
            <Menu.Item key="delete" icon={<DeleteOutlined />}>
              删除
            </Menu.Item>
          )}
        </Menu>
      }
    >
      <a>
        <EllipsisOutlined />
      </a>
    </Dropdown>
  );

  const action = (key: string | number, record: any, index: number) => {
    switch (key) {
      case 'edit':
        editCron(record, index);
        break;
      case 'enableordisable':
        enabledOrDisabledCron(record, index);
        break;
      case 'delete':
        delCron(record, index);
        break;
      default:
        break;
    }
  };

  const handleCancel = (cron?: any) => {
    setIsModalVisible(false);
    if (cron) {
      handleCrons(cron);
    }
  };

  const onSearch = (value: string) => {
    setSearchText(value);
  };

  const handleCrons = (cron: any) => {
    const index = value.findIndex((x) => x._id === cron._id);
    const result = [...value];
    if (index === -1) {
      result.push(cron);
    } else {
      result.splice(index, 1, {
        ...cron,
      });
    }
    setValue(result);
  };

  const getCronDetail = (cron: any) => {
    request
      .get(`${config.apiPrefix}crons/${cron._id}`)
      .then((data: any) => {
        console.log(value);
        const index = value.findIndex((x) => x._id === cron._id);
        console.log(index);
        const result = [...value];
        result.splice(index, 1, {
          ...cron,
          ...data.data,
        });
        setValue(result);
      })
      .finally(() => setLoading(false));
  };

  const onSelectChange = (selectedIds: any[]) => {
    setSelectedRowIds(selectedIds);
  };

  const rowSelection = {
    selectedRowIds,
    onChange: onSelectChange,
    selections: [
      Table.SELECTION_ALL,
      Table.SELECTION_INVERT,
      Table.SELECTION_NONE,
    ],
  };

  const delCrons = () => {
    Modal.confirm({
      title: '确认删除',
      content: <>确认删除选中的定时任务吗</>,
      onOk() {
        request
          .delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
          .then((data: any) => {
            if (data.code === 200) {
              message.success('批量删除成功');
              setSelectedRowIds([]);
              getCrons();
            } else {
              message.error(data);
            }
          });
      },
      onCancel() {
        console.log('Cancel');
      },
    });
  };

  const operateCrons = (operationStatus: number) => {
    Modal.confirm({
      title: `确认${OperationName[operationStatus]}`,
      content: <>确认{OperationName[operationStatus]}选中的定时任务吗</>,
      onOk() {
        request
          .put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
            data: selectedRowIds,
          })
          .then((data: any) => {
            if (data.code === 200) {
              getCrons();
            } else {
              message.error(data);
            }
          });
      },
      onCancel() {
        console.log('Cancel');
      },
    });
  };

  const onPageChange = (page: number, pageSize: number | undefined) => {
    setCurrentPage(page);
    setPageSize(pageSize as number);
    localStorage.setItem('pageSize', pageSize + '');
  };

  useEffect(() => {
    if (logCron) {
      localStorage.setItem('logCron', logCron._id);
      setIsLogModalVisible(true);
    }
  }, [logCron]);

  useEffect(() => {
    getCrons();
  }, [searchText]);

  useEffect(() => {
    if (document.body.clientWidth < 768) {
      setWidth('auto');
      setMarginLeft(0);
      setMarginTop(0);
    } else {
      setWidth('100%');
      setMarginLeft(0);
      setMarginTop(-72);
    }
    setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
  }, []);

  return (
    <PageContainer
      className="ql-container-wrapper crontab-wrapper"
      title="定时任务"
      extra={[
        <Search
          placeholder="请输入名称或者关键词"
          style={{ width: 'auto' }}
          enterButton
          loading={loading}
          onSearch={onSearch}
        />,
        <Button key="2" type="primary" onClick={() => addCron()}>
          添加定时
        </Button>,
      ]}
      header={{
        style: {
          padding: '4px 16px 4px 15px',
          position: 'sticky',
          top: 0,
          left: 0,
          zIndex: 20,
          marginTop,
          width,
          marginLeft,
        },
      }}
    >
      {selectedRowIds.length > 0 && (
        <div style={{ marginBottom: 16 }}>
          <Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
            批量删除
          </Button>
          <Button
            type="primary"
            onClick={() => operateCrons(0)}
            style={{ marginLeft: 8, marginBottom: 5 }}
          >
            批量启用
          </Button>
          <Button
            type="primary"
            onClick={() => operateCrons(1)}
            style={{ marginLeft: 8, marginRight: 8 }}
          >
            批量禁用
          </Button>
          <Button
            type="primary"
            style={{ marginRight: 8 }}
            onClick={() => operateCrons(2)}
          >
            批量运行
          </Button>
          <Button type="primary" onClick={() => operateCrons(3)}>
            批量停止
          </Button>
          <span style={{ marginLeft: 8 }}>
            已选择
            <a>{selectedRowIds?.length}</a>项
          </span>
        </div>
      )}
      <Table
        columns={columns}
        pagination={{
          hideOnSinglePage: true,
          current: currentPage,
          onChange: onPageChange,
          pageSize: pageSize,
          showSizeChanger: true,
          defaultPageSize: 20,
          showTotal: (total: number, range: number[]) =>
            `第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
        }}
        dataSource={value}
        rowKey="_id"
        size="middle"
        scroll={{ x: 768 }}
        loading={loading}
        rowSelection={rowSelection}
      />
      <CronLogModal
        visible={isLogModalVisible}
        handleCancel={() => {
          getCronDetail(logCron);
          setIsLogModalVisible(false);
        }}
        cron={logCron}
      />
      <CronModal
        visible={isModalVisible}
        handleCancel={handleCancel}
        cron={editedCron}
      />
    </PageContainer>
  );
}
Example #9
Source File: GeoDataResolver.tsx    From jitsu with MIT License 4 votes vote down vote up
function GeoDataResolver() {
  const services = useServices()

  const [saving, setSaving] = useState(false)
  const [testingConnection, setTestingConnection] = useState(false)
  const [formDisabled, setFormDisabled] = useState(false)

  const [form] = useForm<GeoDataResolverFormValues>()

  const {
    error: loadingError,
    data: formConfig,
    setData: setFormConfig,
  } = useLoaderAsObject<MaxMindConfig>(async () => {
    const response = await services.backendApiClient.get(
      `/configurations/${geoDataResolversCollection}?id=${services.activeProject.id}`
    )

    let config = {
      license_key: response.maxmind?.license_key,
      enabled: response.maxmind?.enabled,
      editions: [],
    }

    //set statuses or load
    if (response.maxmind?.enabled && response.maxmind?._statuses) {
      config.editions = response.maxmind._statuses
    } else {
      const response = await ApplicationServices.get().backendApiClient.get(
        withQueryParams("/geo_data_resolvers/editions", { project_id: services.activeProject.id }),
        { proxy: true }
      )
      config.editions = response.editions
    }

    form.setFieldsValue({
      license_key: config.license_key,
      enabled: config.enabled,
    })

    setFormDisabled(!config.enabled)

    return config
  }, [])

  const submit = async () => {
    setSaving(true)
    let formValues = form.getFieldsValue()
    try {
      if (formValues.enabled) {
        await testConnection(true)
      }
      await save()
    } catch (error) {
      actionNotification.error(error.message || error)
    } finally {
      setSaving(false)
    }
  }

  const save = async () => {
    let formValues = form.getFieldsValue()

    let config = {
      maxmind: {
        enabled: formValues.enabled,
        license_key: formValues.license_key,
        _statuses: formValues.enabled ? formConfig.editions : null,
      },
    }

    await services.backendApiClient.post(
      `/configurations/${geoDataResolversCollection}?id=${services.activeProject.id}`,
      Marshal.toPureJson(config)
    )

    let anyConnected =
      formConfig.editions.filter(editionStatus => {
        return editionStatus.main.status === "ok" || editionStatus.analog?.status === "ok"
      }).length > 0

    if (!formValues.enabled || anyConnected) {
      actionNotification.success("Settings saved!")
    }

    if (formValues.enabled && !anyConnected) {
      actionNotification.warn(
        `Settings have been saved, but there is no available MaxMind database for this license key. Geo Resolution won't be applied to your JSON events`
      )
    }
  }

  const testConnection = async (hideMessage?: boolean) => {
    setTestingConnection(true)

    let formValues = form.getFieldsValue()

    try {
      const response = await ApplicationServices.get().backendApiClient.post(
        withQueryParams("/geo_data_resolvers/test", { project_id: services.activeProject.id }),
        { maxmind_url: formValues.license_key },
        {
          proxy: true,
        }
      )

      if (response.message) throw new Error(response.message)

      //enrich state
      let currentFormConfig = formConfig
      currentFormConfig.editions = response.editions
      setFormConfig(currentFormConfig)

      //show notification
      if (!hideMessage) {
        let anyConnected =
          formConfig.editions.filter(editionStatus => {
            return editionStatus.main.status === "ok" || editionStatus.analog?.status === "ok"
          }).length > 0

        if (anyConnected) {
          actionNotification.success("Successfully connected!")
        } else {
          actionNotification.error("Connection failed: there is no available MaxMind database for this license key")
        }
      }
    } catch (error) {
      if (!hideMessage) {
        handleError(error, "Connection failed")
      }
    } finally {
      setTestingConnection(false)
    }
  }

  const databaseStatusesRepresentation = (dbStatus: any) => {
    let body = <>-</>
    if (dbStatus) {
      let icon = (
        <Tooltip title="Not connected yet">
          <ClockCircleOutlined className="text-secondaryText" />
        </Tooltip>
      )

      if (dbStatus.status === "ok") {
        icon = (
          <Tooltip title="Successfully connected">
            <CheckCircleOutlined className="text-success" />
          </Tooltip>
        )
      } else if (dbStatus.status === "error") {
        icon = (
          <Tooltip title={dbStatus.message}>
            <CloseCircleOutlined className="text-error" />
          </Tooltip>
        )
      }

      body = (
        <>
          {dbStatus.name}: {icon}
        </>
      )
    }

    return body
  }

  if (loadingError) {
    return <CenteredError error={loadingError} />
  } else if (!formConfig) {
    return <CenteredSpin />
  }

  return (
    <div className="flex justify-center w-full">
      <div className="w-full pt-8 px-4" style={{ maxWidth: "1000px" }}>
        <p>
          Jitsu uses <a href="https://www.maxmind.com/">MaxMind</a> databases for geo resolution. There are two families
          of MaxMind databases: <b>GeoIP2</b> and <b>GeoLite2</b>. After setting a license key{" "}
          <b>all available MaxMind databases, which the license key has access</b>, will be downloaded and used for
          enriching incoming events. For using a certain database add{" "}
          <CodeInline>{"?edition_id=<database type>"}</CodeInline> to MaxMind License Key value. For example:{" "}
          <CodeInline>{"M10sDzWKmnDYUBM0?edition_id=GeoIP2-City,GeoIP2-ISP"}</CodeInline>.
        </p>

        <div className="w-96 flex-wrap flex justify-content-center">
          <Table
            pagination={false}
            columns={[
              {
                title: (
                  <>
                    Database{" "}
                    <Tooltip title="Paid MaxMind Database">
                      <QuestionCircleOutlined className="label-with-tooltip_question-mark" />
                    </Tooltip>
                  </>
                ),
                dataIndex: "main",
                key: "name",
                render: databaseStatusesRepresentation,
              },
              {
                title: (
                  <>
                    Analog{" "}
                    <Tooltip title="Free MaxMind Database analog. Usually it is less accurate than paid version. It is downloaded only if paid one is unavailable.">
                      <QuestionCircleOutlined className="label-with-tooltip_question-mark" />
                    </Tooltip>
                  </>
                ),
                dataIndex: "analog",
                key: "name",
                render: databaseStatusesRepresentation,
              },
            ]}
            dataSource={formConfig.editions}
          />
        </div>

        <br />
        <Form form={form} onFinish={submit}>
          <FormLayout>
            <FormField
              label="Enabled"
              tooltip={
                <>
                  If enabled - Jitsu downloads <a href="https://www.maxmind.com/en/geoip2-databases">GeoIP Databases</a>{" "}
                  with your license key and enriches incoming JSON events with location based data. Read more
                  information about{" "}
                  <a href="https://jitsu.com/docs/other-features/geo-data-resolution">Geo data resolution</a>.
                </>
              }
              key="enabled"
            >
              <Form.Item name="enabled" valuePropName="checked">
                <Switch
                  onChange={value => {
                    setFormDisabled(!value)
                  }}
                  size="default"
                />
              </Form.Item>
            </FormField>
            <FormField
              label="MaxMind License Key"
              tooltip={
                <>
                  Your MaxMind licence key. Obtain a new one in your <a href="https://www.maxmind.com/">Account</a>{" "}
                  {"->"} Manage License Keys. Jitsu downloads all available MaxMind databases with your license key. If
                  you would like to enrich events JSON with the only certain MaxMind DB data{": "}
                  specify license key with the format:{" "}
                  {"<license_key>?edition_id=<comma separated editions like: GeoIP2-City,GeoIP2-ISP>"}. If you use{" "}
                  <a href="https://cloud.jitsu.com/">Jitsu.Cloud</a> and MaxMind isn't set - free GeoLite2-City and
                  GeoLite2-ASN MaxMind databases are applied. Read more about{" "}
                  <a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data?lang=en">
                    free MaxMind databases
                  </a>
                  .{" "}
                </>
              }
              key="license_key"
            >
              <Form.Item name="license_key">
                <Input
                  disabled={formDisabled}
                  size="large"
                  name="license_key"
                  placeholder="for example: M10sDzWKmnDYUBM0"
                  required={true}
                />
              </Form.Item>
            </FormField>
            <FormActions>
              <Button
                size="large"
                className="mr-3"
                type="dashed"
                loading={testingConnection}
                onClick={() => testConnection()}
                icon={<ApiOutlined />}
                disabled={formDisabled}
              >
                Test connection
              </Button>
              <Button loading={saving} htmlType="submit" size="large" type="primary">
                Save
              </Button>
            </FormActions>
          </FormLayout>
        </Form>
      </div>
    </div>
  )
}
Example #10
Source File: detail.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
CustomerDetail: React.FC = () => {
  const queryFormRef = useRef<FormInstance>();
  const actionRef = useRef<ActionType>();
  const [customerDetail, setCustomerDetail] = useState<CustomerItem>()
  const [staffMap, setStaffMap] = useState<Dictionary<StaffOption>>({});
  const [allCustomerTagGroups, setAllCustomerTagGroups] = useState<CustomerTagGroupItem[]>([]);
  const [defaultCustomerTags, setDefaultCustomerTags] = useState<CustomerTag[]>([]);
  const [defaultInternalTagsIds, setDefaultInternalTagsIds] = useState<string []>([])
  const [personalTagModalVisible, setPersonalTagModalVisible] = useState(false)
  const [customerTagModalVisible, setCustomerTagModalVisible] = useState(false)
  const [internalTagList, setInternalTagList] = useState<InternalTags.Item[]>([])
  const [internalTagListMap, setInternalTagListMap] = useState<Dictionary<InternalTags.Item>>({});
  const [initialEvents, setInitialEvents] = useState<CustomerEvents.Item[]>([])
  const [currentTab, setCurrentTab] = useState('survey')
  const [basicInfoDisplay, setBasicInfoDisplay] = useState({} as any)// 展示哪些基本信息
  const [basicInfoValues, setBasicInfoValues] = useState({} as any) // 基本信息取值
  const [remarkValues, setRemarkValues] = useState<Remark[]>([])
  const [reloadCusDataTimesTamp, setReloadCusDataTimesTamp] = useState(Date.now)
  const [formRef] = Form.useForm()

  const params = new URLSearchParams(window.location.search);
  const currentStaff = localStorage.getItem('extStaffAdminID') as string
  const extCustomerID = params.get('ext_customer_id') || "";
  if (!extCustomerID) {
    message.error('传入参数请带上ID');
  }

  const extStaff = () => {
    const staffs: StaffItem[] = [];
    customerDetail?.staff_relations?.forEach((staff_relation) => {
      // @ts-ignore
      const staff = staffMap[staff_relation.ext_staff_id];
      if (staff) {
        staffs.push(staff);
      }
    });
    return staffs;
  }

  const getCustomerDetail = () => {
    const hide = message.loading("加载数据中");
    GetCustomerDetail(extCustomerID).then(res => {
      hide();
      if (res?.code !== 0) {
        message.error("获取客户详情失败");
        return;
      }
      setCustomerDetail(res?.data);
      const cusTags: any[] = [];
      const interTagsIds: any[] = []
      res?.data?.staff_relations?.forEach((relation: any) => {
        if (relation.ext_staff_id === currentStaff) {
          relation.customer_staff_tags?.forEach((tag: any) => {
            cusTags.push({...tag, name: tag.tag_name, ext_id: tag.ext_tag_id});
          });
          relation.internal_tags?.forEach((tagId: string) => {
            interTagsIds.push(tagId);
          })
        }

      });
      setDefaultCustomerTags(cusTags)
      setDefaultInternalTagsIds(interTagsIds)
    }).catch(() => {
      hide();
    })
  }

  const getInternalTags = () => {
    QueryInternalTags({page_size: 5000, ext_staff_id: currentStaff}).then(res => {
      if (res?.code === 0) {
        setInternalTagList(res?.data?.items)
        setInternalTagListMap(_.keyBy(res?.data?.items, 'id'))
      } else {
        message.error(res?.message)
      }
    })
  }

  const getCustomerRemark = () => { // 自定义信息id-key
    QueryCustomerRemark().then(res => {
      if (res?.code === 0) {
        console.log('QueryCustomerRemark', res.data)
      } else {
        message.error(res?.message)
      }
    })
  }

  const getBasicInfoDisplay = () => {
    GetCustomerBasicInfoDisplay().then(res => {
      if (res?.code === 0) {
        const displayData = res?.data
        delete displayData.id
        delete displayData.ext_corp_id
        delete displayData.created_at
        delete displayData.updated_at
        delete displayData.deleted_at
        setBasicInfoDisplay(displayData || {})
      } else {
        message.error(res?.message)
      }
    })
  }

  const getBasicInfoAndRemarkValues = () => {
    GetBasicInfoAndRemarkValues({
      ext_customer_id: extCustomerID,
      ext_staff_id: currentStaff,
    }).then(res => {
      if (res?.code === 0) {
        const resData = res?.data
        delete resData.id
        delete resData.ext_corp_id
        delete resData.ext_creator_id
        delete resData.ext_customer_id
        delete resData.ext_staff_id
        delete resData.created_at
        delete resData.updated_at
        delete resData.deleted_at
        delete resData.remark_values
        setBasicInfoValues(resData)
        setRemarkValues(res?.data?.remark_values || [])
      }
    })
  }

  const updateBasicInfoAndRemark = (basicInfoParams: any) => {
    UpdateBasicInfoAndRemark({
      ext_staff_id: currentStaff,
      ext_customer_id: extCustomerID,
      ...basicInfoParams
    }).then(res => {
      if (res?.code === 0) {
        message.success('客户信息更新成功')
        setReloadCusDataTimesTamp(Date.now)
      } else {
        message.error('客户信息更新失败')
      }
    })
  }

  useEffect(() => {
    getInternalTags()
    getCustomerDetail()
    getCustomerRemark()
    getBasicInfoDisplay()
    getBasicInfoAndRemarkValues()
  }, [reloadCusDataTimesTamp])

  useEffect(() => {
    QueryCustomerTagGroups({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        setAllCustomerTagGroups(res?.data?.items);
      } else {
        message.error(res.message);
      }
    });
  }, []);

  useEffect(() => {
    QuerySimpleStaffs({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        const staffs = res?.data?.items?.map((item: SimpleStaffInterface) => {
          return {
            label: item.name,
            value: item.ext_id,
            ...item,
          };
        }) || [];
        setStaffMap(_.keyBy<StaffOption>(staffs, 'ext_id'));
      } else {
        message.error(res.message);
      }
    });
  }, []);

  useEffect(() => {
    QueryCustomerEvents({
      ext_customer_id: extCustomerID,
      ext_staff_id: currentStaff,
      page_size: 5
    }).then(res => {
      console.log('QueryCustomerEventsQueryCustomerEvents', res)
      setInitialEvents(res?.data?.items || [])
    })
  }, [])

  useEffect(() => {
    formRef.setFieldsValue(basicInfoValues)
  }, [basicInfoValues])

  return (
    <PageContainer
      fixedHeader
      onBack={() => history.go(-1)}
      backIcon={<LeftOutlined/>}
      header={{
        title: '客户详情',
      }}
    >
      <ProCard>
        <Descriptions title="客户信息" column={1}>
          <Descriptions.Item>
            <div className={'customer-info-field'}>
              <div><img src={customerDetail?.avatar} alt={customerDetail?.name} style={{borderRadius: 5}}/></div>
              <div style={{fontSize: 16, marginLeft: 10}}>
                <p>{customerDetail?.name}</p>
                {customerDetail?.corp_name && (
                  <p style={{color: '#eda150', marginTop: 10}}>@{customerDetail?.corp_name}</p>
                )}
                {customerDetail?.type === 1 && (
                  <p style={{
                    color: '#5ec75d',
                    fontSize: '13px'
                  }}>@微信</p>
                )}
              </div>
            </div>
          </Descriptions.Item>
          <div>
            <div style={{width: 70, display: 'inline-block'}}>企业标签:</div>
            <div className={styles.tagContainer}>
              <Space direction={'horizontal'} wrap={true}>
                {
                  defaultCustomerTags?.length > 0 && defaultCustomerTags?.map((tag) =>
                    <Tag
                      key={tag?.id}
                      className={'tag-item selected-tag-item'}
                    >
                      {tag?.name}
                    </Tag>
                  )}
              </Space>
            </div>
            <Button
              key='addCusTags'
              icon={<EditOutlined/>}
              type={'link'}
              onClick={() => {
                setCustomerTagModalVisible(true);
              }}
            >
              编辑
            </Button>
          </div>

          <div>
            <div style={{width: 70, display: 'inline-block'}}>个人标签:</div>
            <div className={styles.tagContainer}>
              <Space direction={'horizontal'} wrap={true}>
                {
                  defaultInternalTagsIds?.length > 0 && defaultInternalTagsIds.map(id => internalTagListMap[id])?.map((tag) =>
                    <Tag
                      key={tag?.id}
                      className={'tag-item selected-tag-item'}
                    >
                      {tag?.name}
                      <span>
                     </span>
                    </Tag>
                  )}
              </Space>
            </div>
            <Button
              key='addInternalTags'
              icon={<EditOutlined/>}
              type={'link'}
              onClick={() => {
                setPersonalTagModalVisible(true);
              }}
            >
              编辑
            </Button>
          </div>
        </Descriptions>
      </ProCard>

      <ProCard
        tabs={{
          onChange: (activeKey: string) => setCurrentTab(activeKey),
          activeKey: currentTab
        }}
        style={{marginTop: 25}}
      >
        <ProCard.TabPane key="survey" tab="客户概况">
          <div className={styles.survey}>
            <div className={styles.cusSurveyLeft}>
              <div>
                <Descriptions title={<div><ContactsFilled/>&nbsp;&nbsp;添加客服信息</div>} layout="vertical" bordered
                              column={4}>
                  <Descriptions.Item label="所属员工">
                    <CollapsedStaffs limit={2} staffs={extStaff()}/>
                  </Descriptions.Item>

                  <Descriptions.Item label="客户来源">
                       <span>
                         {customerDetail?.staff_relations?.map((para) => {
                           return (`${addWayEnums[para.add_way || 0]}\n`);
                         })}
                       </span>
                  </Descriptions.Item>
                  <Descriptions.Item label="添加时间">
                    <Space>
                      {customerDetail?.staff_relations?.map((para) => {
                        return (
                          <div className={styles.staffTag}
                               dangerouslySetInnerHTML={{
                                 __html: moment(para.createtime)
                                   .format('YYYY-MM-DD HH:mm')
                                   .split(' ')
                                   .join('<br />'),
                               }}
                          />
                        );
                      })}
                    </Space>
                  </Descriptions.Item>
                  <Descriptions.Item label="更新时间">
                    <div
                      dangerouslySetInnerHTML={{
                        __html: moment(customerDetail?.updated_at)
                          .format('YYYY-MM-DD HH:mm')
                          .split(' ')
                          .join('<br />'),
                      }}
                    />
                  </Descriptions.Item>
                </Descriptions>
              </div>
              <Form form={formRef} onFinish={(values) => {
                console.log('ooooooooooooooovaluesvalues', values)
                const basicInfoParams = {...values}
                updateBasicInfoAndRemark(basicInfoParams)
              }}>
                <div style={{paddingTop: 20}} className={styles.baseInfoContainer}>
                  <Descriptions
                    title={<div><BookFilled/>&nbsp;&nbsp;基本信息</div>}
                    bordered
                    column={2}
                    size={'small'}
                  >
                    {
                      Object.keys(basicInfoDisplay).map(key => {
                        return <Descriptions.Item label={basicInfo[key]}>
                          <TableInput name={key} />
                        </Descriptions.Item>
                      })
                    }
                  </Descriptions>
                </div>
                {
                  remarkValues.length > 0 && <div style={{paddingTop: 20}} className={styles.customInfoContainer}>
                    <Descriptions
                      title={<div><EditFilled/>&nbsp;&nbsp;自定义信息</div>}
                      bordered
                      column={2}
                      size={'small'}
                    >
                      <Descriptions.Item label="sfdsf">
                        <TableInput name={'aa'}/>
                      </Descriptions.Item>
                      <Descriptions.Item label="违法的">
                        <TableInput name={'bb'}/>
                      </Descriptions.Item>
                      <Descriptions.Item label="sdf434">
                        <TableInput name={'cc'}/>
                      </Descriptions.Item>
                      <Descriptions.Item label="yjkyujy">
                        <TableInput name={'dd'}/>
                      </Descriptions.Item>
                    </Descriptions>
                  </div>
                }
                <div style={{display: 'flex', justifyContent: 'center', marginTop: 40}}>
                  <Space>
                    <Button onClick={() => formRef.setFieldsValue(basicInfoValues)}>重置</Button>
                    <Button type={"primary"} onClick={() => {
                      formRef.submit()
                    }}>提交</Button>
                  </Space>
                </div>
              </Form>
            </div>

            <div className={styles.cusSurveyRight}>
              <div className={styles.eventsTitle}>
                <span className={styles.titleText}><SoundFilled/>&nbsp;&nbsp;客户动态</span>
                <a onClick={() => setCurrentTab('events')} style={{fontSize: 12}}>查看更多<RightOutlined/></a>
              </div>
              <Events data={initialEvents.filter(elem => elem !== null)} simpleRender={true} staffMap={staffMap}
                      extCustomerID={extCustomerID}/>
            </div>
          </div>

        </ProCard.TabPane>

        <ProCard.TabPane key="events" tab="客户动态">
          <Events staffMap={staffMap} extCustomerID={extCustomerID}/>
        </ProCard.TabPane>

        <ProCard.TabPane key="room" tab="所在群聊">
          <ProTable<GroupChatItem>
            search={false}
            formRef={queryFormRef}
            actionRef={actionRef}
            className={'table'}
            scroll={{x: 'max-content'}}
            columns={columns}
            rowKey="id"
            toolBarRender={false}
            bordered={false}
            tableAlertRender={false}
            dateFormatter="string"
            request={async (originParams: any, sort, filter) => {
              return ProTableRequestAdapter(
                originParams,
                sort,
                filter,
                QueryCustomerGroupsList,
              );
            }}
          />
        </ProCard.TabPane>

        <ProCard.TabPane key="chat" tab="聊天记录">
          {
            setStaffMap[currentStaff]?.enable_msg_arch === 1 ? <Button
                key={'chatSession'}
                type={"link"}
                icon={<ClockCircleOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
                onClick={() => {
                  window.open(`/staff-admin/corp-risk-control/chat-session?staff=${currentStaff}`)
                }}
              >
                聊天记录查询
              </Button>
              :
              <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={<span>员工暂未开启消息存档</span>}/>
          }
        </ProCard.TabPane>

      </ProCard>

      <CustomerTagSelectionModal
        type={'customerDetailEnterpriseTag'}
        isEditable={true}
        withLogicalCondition={false}
        width={'630px'}
        visible={customerTagModalVisible}
        setVisible={setCustomerTagModalVisible}
        defaultCheckedTags={defaultCustomerTags}
        onFinish={(selectedTags) => {
          const removeAry = _.difference(defaultCustomerTags.map(dt => dt.ext_id), selectedTags.map(st => st.ext_id))
          UpdateCustomerTags({
            // @ts-ignore
            add_ext_tag_ids: selectedTags.map((tag) => tag.ext_id),
            ext_customer_ids: [extCustomerID],
            ext_staff_id: currentStaff,
            // @ts-ignore
            remove_ext_tag_ids: removeAry
          }).then(() => {
            getCustomerDetail()
          })
        }}
        allTagGroups={allCustomerTagGroups}
      />

      <InternalTagModal
        width={560}
        allTags={internalTagList}
        allTagsMap={internalTagListMap}
        setAllTags={setInternalTagList}
        visible={personalTagModalVisible}
        setVisible={setPersonalTagModalVisible}
        defaultCheckedTagsIds={defaultInternalTagsIds}
        reloadTags={getInternalTags}
        onFinish={(selectedTags) => {
          console.log('selectedTags', selectedTags)
          const removeAry = _.difference(defaultInternalTagsIds, selectedTags.map(st => st.id))
          CustomerInternalTags({
            // @ts-ignore
            add_ext_tag_ids: selectedTags.map((tag) => tag.id),
            ext_customer_id: extCustomerID,
            ext_staff_id: currentStaff,
            // @ts-ignore
            remove_ext_tag_ids: removeAry
          }).then(() => {
            getCustomerDetail()
          })
        }
        }
      />
    </PageContainer>
  );
}
Example #11
Source File: PopularTimes.tsx    From office-hours with GNU General Public License v3.0 4 votes vote down vote up
export default function PopularTimes({ heatmap }: HeatmapProps): ReactElement {
  const [currentDayOfWeek, setCurrentDayOfWeek] = useState(new Date().getDay());
  const [firstHour, lastHour] = findWeekMinAndMax(heatmap);
  const dailyAvgWaitTimes: number[] = chunk(heatmap, 24).map((hours) => {
    const filteredOfficeHours = hours.filter((v) => v !== -1);
    return filteredOfficeHours.length > 0 ? mean(filteredOfficeHours) : -1;
  });

  return (
    <div className="hide-in-percy">
      <TitleRow>
        <h2>Wait Times on</h2>
        <Dropdown
          trigger={["click"]}
          overlay={
            <Menu>
              {DAYS_OF_WEEK.map((dayName, i) => (
                <Menu.Item key={dayName}>
                  <a onClick={() => setCurrentDayOfWeek(i)}>{dayName}</a>
                </Menu.Item>
              ))}
            </Menu>
          }
        >
          <WeekdayDropdown>
            {DAYS_OF_WEEK[currentDayOfWeek]}
            <DownOutlined />
          </WeekdayDropdown>
        </Dropdown>
      </TitleRow>
      <GraphWithArrow>
        <GraphArrowButtons
          onClick={() => setCurrentDayOfWeek((7 + currentDayOfWeek - 1) % 7)}
        >
          <LeftOutlined />
        </GraphArrowButtons>
        <GraphContainer>
          <ParentSize>
            {({ width }) => (
              <TimeGraph
                values={heatmap
                  .slice(currentDayOfWeek * 24, (currentDayOfWeek + 1) * 24 - 1)
                  .map((i) => (i < 0 ? 0 : Math.floor(i)))}
                maxTime={Math.max(...heatmap)}
                firstHour={firstHour}
                lastHour={lastHour}
                width={width}
                height={220}
              />
            )}
          </ParentSize>
        </GraphContainer>
        <GraphArrowButtons
          onClick={() => setCurrentDayOfWeek((currentDayOfWeek + 1) % 7)}
        >
          <RightOutlined />
        </GraphArrowButtons>
      </GraphWithArrow>
      {dailyAvgWaitTimes[currentDayOfWeek] >= 0 && (
        <GraphNotes>
          <ClockCircleOutlined /> {DAYS_OF_WEEK[currentDayOfWeek]}s have{" "}
          <strong>
            {generateBusyText(currentDayOfWeek, dailyAvgWaitTimes)}
          </strong>{" "}
          wait times.
        </GraphNotes>
      )}
      {new Date().getDay() === currentDayOfWeek &&
        heatmap[currentDayOfWeek * 24 + new Date().getHours()] >= 0 && (
          <GraphNotes>
            <HourglassOutlined /> At {formatDateHour(new Date().getHours())},
            people generally wait{" "}
            <strong>
              {formatWaitTime(
                heatmap[currentDayOfWeek * 24 + new Date().getHours()]
              )}
            </strong>
            .
          </GraphNotes>
        )}
    </div>
  );
}
Example #12
Source File: App.tsx    From pcap2socks-gui with MIT License 4 votes vote down vote up
renderRunning = () => {
    return (
      <div className="content-content">
        <Row className="content-content-row" gutter={[16, 16]} justify="center">
          <Col className="content-content-col" span={24}>
            {(() => {
              if (Number.isNaN(this.state.time)) {
                return <QuestionCircleTwoTone className="content-content-icon" />;
              } else {
                return <CheckCircleTwoTone className="content-content-icon" twoToneColor="#52c41a" />;
              }
            })()}
          </Col>
        </Row>
        <Row className="content-content-row" gutter={[16, 32]} justify="center">
          <Col className="content-content-col" span={24}>
            <Paragraph>
              <Title level={3}>
                {(() => {
                  if (Number.isNaN(this.state.time)) {
                    return "未运行";
                  } else {
                    return "运行中";
                  }
                })()}
              </Title>
            </Paragraph>
          </Col>
        </Row>
        <Row gutter={[16, 0]} justify="center">
          <Col xs={24} sm={12} md={6} style={{ marginBottom: "16px" }}>
            <Card className="card" hoverable>
              <Statistic
                precision={2}
                prefix={<ClockCircleOutlined />}
                title="运行时间"
                value={Convert.convertTime(this.state.time)}
              />
            </Card>
          </Col>
          <Col xs={24} sm={12} md={6} style={{ marginBottom: "16px" }}>
            <Card className="card" hoverable>
              <Statistic
                prefix={<HourglassOutlined />}
                title="延迟"
                value={Convert.convertDuration(this.state.latency)}
                valueStyle={(() => {
                  if (this.state.latency === Infinity) {
                    return { color: "#cf1322" };
                  } else if (this.state.latency >= 100) {
                    return { color: "#faad14" };
                  }
                })()}
                suffix={Convert.convertDurationUnit(this.state.latency)}
              />
            </Card>
          </Col>
          <Col xs={24} sm={12} md={6} style={{ marginBottom: "16px" }}>
            <Card hoverable onClick={this.switchTraffic}>
              <Statistic
                precision={2}
                prefix={<ArrowUpOutlined />}
                title="上传"
                value={this.showUploadValue()}
                suffix={this.showUploadUnit()}
              />
            </Card>
          </Col>
          <Col xs={24} sm={12} md={6} style={{ marginBottom: "16px" }}>
            <Card hoverable onClick={this.switchTraffic}>
              <Statistic
                precision={2}
                prefix={<ArrowDownOutlined />}
                title="下载"
                value={this.showDownloadValue()}
                suffix={this.showDownloadUnit()}
              />
            </Card>
          </Col>
        </Row>
      </div>
    );
  };