lodash#head TypeScript Examples

The following examples show how to use lodash#head. 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: loaders-explorer.service.ts    From nestjs-mercurius with MIT License 6 votes vote down vote up
private registerContextProvider<T = any>(request: T, contextId: ContextId) {
    const coreModuleArray = [...this.modulesContainer.entries()]
      .filter(
        ([key, { metatype }]) =>
          metatype && metatype.name === InternalCoreModule.name,
      )
      .map(([key, value]) => value);

    const coreModuleRef = head(coreModuleArray);
    if (!coreModuleRef) {
      return;
    }
    const wrapper = coreModuleRef.getProviderByKey(REQUEST);
    wrapper.setInstanceByContextId(contextId, {
      instance: request,
      isResolved: true,
    });
  }
Example #2
Source File: BuildHeader.tsx    From amplication with Apache License 2.0 6 votes vote down vote up
BuildHeader = ({ build, deployments, isError }: Props) => {
  const deployedClassName = `${CLASS_NAME}--deployed`;

  const deployment = head(deployments);

  const isDeployed =
    deployment && deployment.status === models.EnumDeploymentStatus.Completed;

  return (
    <div className={`${CLASS_NAME} ${isDeployed && deployedClassName} `}>
      <ClickableId
        to={`/${build.appId}/builds/${build.id}`}
        id={build.id}
        label="Build ID"
        eventData={{
          eventName: "buildHeaderIdClick",
        }}
      />
      {isError ? (
        <Link to={`/${build.appId}/builds/${build.id}`}>
          <h3 className="error-message">Build Failed Check Logs</h3>
        </Link>
      ) : null}
      <span className="spacer" />
      {deployment && isDeployed && (
        <a href={deployment.environment.address} target="app">
          <Icon icon="link_2" />
        </a>
      )}
    </div>
  );
}
Example #3
Source File: row-text-click.ts    From S2 with MIT License 6 votes vote down vote up
private getRowIndex = (cellData: Node) => {
    const isTree = this.spreadsheet.options.hierarchyType === 'tree';
    if (isTree) {
      let child = cellData;
      while (!isEmpty(child.children)) {
        child = head(child.children);
      }
      return cellData.rowIndex ?? child.rowIndex;
    }
    // if current cell has no row index, return dynamic computed value
    const rowIndex = Math.floor(cellData.y / cellData.height);
    return cellData.rowIndex ?? rowIndex;
  };
Example #4
Source File: traceSummary.ts    From erda-ui with GNU Affero General Public License v3.0 6 votes vote down vote up
// What's the total duration of the spans in this trace?
// 最后结束的(时间戳+耗时最大)- 第一个调用的时间戳
export function traceDuration(spans: any) {
  function makeList({ timestamp, duration }: any) {
    if (!timestamp) {
      return [];
    } else if (!duration) {
      return [timestamp];
    }
    return [timestamp, timestamp + duration];
  }
  // turns (timestamp, timestamp + duration) into an ordered list
  const timestamps = fp.flow(fp.flatMap(makeList), fp.sortBy(identity))(spans);

  if (timestamps.length < 2) {
    return null;
  }
  const firstTime = head(timestamps);
  const lastTime = last(timestamps);
  return lastTime - firstTime;
}
Example #5
Source File: flow-right.spec.ts    From s-libs with MIT License 6 votes vote down vote up
describe('flowRight()', () => {
  //
  // stolen from https://github.com/lodash/lodash
  //

  it('should supply each function with the return value of the previous', () => {
    const increment = (x: number): number => x + 1;
    const square = (x: number): number => x * x;
    const fixed = (n: number): string => n.toFixed(1);

    expect(flowRight(fixed, square, increment)(2)).toBe('9.0');
  });

  it('should return an identity function when no arguments are given', () => {
    expect(flowRight()('a')).toBe('a');
  });

  it('should work with a curried function and `_.head`', () => {
    const curried: any = curry(identity);
    const combined: any = flowRight(head as any, curried);

    expect(combined([1])).toBe(1);
  });
});
Example #6
Source File: flow.spec.ts    From s-libs with MIT License 6 votes vote down vote up
describe('flow()', () => {
  //
  // stolen from https://github.com/lodash/lodash
  //

  it('should supply each function with the return value of the previous', () => {
    const increment = (x: number): number => x + 1;
    const square = (x: number): number => x * x;
    const fixed = (n: number): string => n.toFixed(1);

    expect(flow(increment, square, fixed)(2)).toBe('9.0');
  });

  it('should return an identity function when no arguments are given', () => {
    expect(flow()('a')).toBe('a');
  });

  it('should work with a curried function and `_.head`', () => {
    const curried: any = curry(identity);
    const combined: any = flow(head as any, curried);
    expect(combined([1])).toBe(1);
  });
});
Example #7
Source File: common-notify-group.tsx    From erda-ui with GNU Affero General Public License v3.0 5 votes vote down vote up
ListTargets = ({
  targets = [],
  roleMap,
}: {
  targets: COMMON_STRATEGY_NOTIFY.INotifyTarget[];
  roleMap: any;
}) => {
  const userMap = useUserMap();
  const { values = [], type } = targets[0] || {};
  const firstValue = head(values)?.receiver as string;
  let text = '';
  let targetsEle = (
    <>
      <ErdaIcon fill="black-4" size="16" type="sidebarUser" className="color-text-desc mr-1" />
      <Tooltip title={`${i18n.t('dop:group address')}: ${firstValue}`}>
        <span className="group-address nowrap">{`${i18n.t('dop:group address')}: ${firstValue}`}</span>
      </Tooltip>
    </>
  );
  switch (type) {
    case TargetType.USER:
      text = `${userMap[firstValue] ? userMap[firstValue].nick : '--'} ${i18n.t('dop:and {length} others', {
        length: values.length,
      })}`;
      targetsEle = (
        <>
          <div className="group-members mr-2 flex">
            {map(take(values, 6), (obj: { receiver: string }) => (
              <UserInfo.RenderWithAvatar id={obj.receiver} key={obj.receiver} showName={false} />
            ))}
          </div>
          <Tooltip title={text}>
            <span className="nowrap">{text}</span>
          </Tooltip>
        </>
      );
      break;
    case TargetType.EXTERNAL_USER:
      text = `${JSON.parse(firstValue).username} ${i18n.t('dop:and {length} others', {
        length: values.length,
      })}`;
      targetsEle = (
        <>
          <div className="group-members mr-2">
            {map(take(values, 3), (obj: { receiver: string }) => {
              const { username } = JSON.parse(obj.receiver);
              return (
                <Avatar size={'small'} key={username}>
                  {getAvatarChars(username)}
                </Avatar>
              );
            })}
          </div>
          <Tooltip title={text}>
            <span className="nowrap">{text}</span>
          </Tooltip>
        </>
      );
      break;
    case TargetType.ROLE:
      text = `${i18n.t('dop:notify role')}:${map(values, (obj) => roleMap[obj.receiver]).join(',')}`;
      targetsEle = (
        <>
          <ErdaIcon fill="black-4" size="16" type="sidebarUser" className="mr-1" />
          <Tooltip title={text}>
            <span className="group-address nowrap">{text}</span>
          </Tooltip>
        </>
      );
      break;
    default:
      break;
  }
  return targetsEle;
}
Example #8
Source File: TestDetailsModal.tsx    From frontend with Apache License 2.0 4 votes vote down vote up
TestDetailsModal: React.FunctionComponent<{
  testRun: TestRun;
  touched: boolean;
  handleClose: () => void;
}> = ({ testRun, touched, handleClose }) => {
  const classes = useStyles();
  const navigate = useNavigate();
  const { enqueueSnackbar } = useSnackbar();
  const testRunDispatch = useTestRunDispatch();

  const stageWidth = (window.innerWidth / 2) * 0.8;
  const stageHeigth = window.innerHeight * 0.6;
  const stageScaleBy = 1.2;
  const [stageScale, setStageScale] = React.useState(1);
  const [stagePos, setStagePos] = React.useState(defaultStagePos);
  const [stageInitPos, setStageInitPos] = React.useState(defaultStagePos);
  const [stageOffset, setStageOffset] = React.useState(defaultStagePos);
  const [processing, setProcessing] = React.useState(false);
  const [isDrawMode, setIsDrawMode] = useState(false);
  const [valueOfIgnoreOrCompare, setValueOfIgnoreOrCompare] = useState(
    "Ignore Areas"
  );
  const [isDiffShown, setIsDiffShown] = useState(false);
  const [selectedRectId, setSelectedRectId] = React.useState<string>();
  const [ignoreAreas, setIgnoreAreas] = React.useState<IgnoreArea[]>([]);
  const [applyIgnoreDialogOpen, setApplyIgnoreDialogOpen] = React.useState(
    false
  );

  const toggleApplyIgnoreDialogOpen = () => {
    setApplyIgnoreDialogOpen(!applyIgnoreDialogOpen);
  };

  const [image, imageStatus] = useImage(
    staticService.getImage(testRun.imageName)
  );
  const [baselineImage, baselineImageStatus] = useImage(
    staticService.getImage(testRun.baselineName)
  );
  const [diffImage, diffImageStatus] = useImage(
    staticService.getImage(testRun.diffName)
  );

  const applyIgnoreAreaText =
    "Apply selected ignore area to all images in this build.";

  React.useEffect(() => {
    fitStageToScreen();
    // eslint-disable-next-line
  }, [image]);

  React.useEffect(() => {
    setIsDiffShown(!!testRun.diffName);
  }, [testRun.diffName]);

  React.useEffect(() => {
    setIgnoreAreas(JSON.parse(testRun.ignoreAreas));
  }, [testRun]);

  const isImageSizeDiffer = React.useMemo(
    () =>
      testRun.baselineName &&
      testRun.imageName &&
      (image?.height !== baselineImage?.height ||
        image?.width !== baselineImage?.width),
    [image, baselineImage, testRun.baselineName, testRun.imageName]
  );

  const handleIgnoreAreaChange = (ignoreAreas: IgnoreArea[]) => {
    setIgnoreAreas(ignoreAreas);
    testRunDispatch({
      type: "touched",
      payload: testRun.ignoreAreas !== JSON.stringify(ignoreAreas),
    });
  };

  const removeSelection = (event: KonvaEventObject<MouseEvent>) => {
    // deselect when clicked not on Rect
    const isRectClicked = event.target.className === "Rect";
    if (!isRectClicked) {
      setSelectedRectId(undefined);
    }
  };

  const deleteIgnoreArea = (id: string) => {
    handleIgnoreAreaChange(ignoreAreas.filter((area) => area.id !== id));
    setSelectedRectId(undefined);
  };

  const saveTestRun = (ignoreAreas: IgnoreArea[], successMessage: string) => {
    testRunService
      .updateIgnoreAreas({
        ids: [testRun.id],
        ignoreAreas,
      })
      .then(() => {
        enqueueSnackbar(successMessage, {
          variant: "success",
        });
      })
      .catch((err) =>
        enqueueSnackbar(err, {
          variant: "error",
        })
      );
  };

  const saveIgnoreAreasOrCompareArea = () => {
    if (valueOfIgnoreOrCompare.includes("Ignore")) {
      saveTestRun(ignoreAreas, "Ignore areas are updated.");
    } else {
      const invertedIgnoreAreas = invertIgnoreArea(
        image!.width,
        image!.height,
        head(ignoreAreas)
      );

      handleIgnoreAreaChange(invertedIgnoreAreas);
      saveTestRun(
        invertedIgnoreAreas,
        "Selected area has been inverted to ignore areas and saved."
      );
    }
    testRunDispatch({ type: "touched", payload: false });
  };

  const onIgnoreOrCompareSelectChange = (value: string) => {
    if (value.includes("Compare")) {
      setValueOfIgnoreOrCompare("Compare Area");
    } else {
      setValueOfIgnoreOrCompare("Ignore Areas");
    }
  };

  const setOriginalSize = () => {
    setStageScale(1);
    resetPositioin();
  };

  const fitStageToScreen = () => {
    const scale = image
      ? Math.min(
          stageWidth < image.width ? stageWidth / image.width : 1,
          stageHeigth < image.height ? stageHeigth / image.height : 1
        )
      : 1;
    setStageScale(scale);
    resetPositioin();
  };

  const resetPositioin = () => {
    setStagePos(defaultStagePos);
    setStageOffset(defaultStagePos);
  };

  const applyIgnoreArea = () => {
    let newIgnoreArea = ignoreAreas.find((area) => selectedRectId! === area.id);
    if (newIgnoreArea) {
      setProcessing(true);
      testRunService
        .getList(testRun.buildId)
        .then((testRuns: TestRun[]) => {
          let allIds = testRuns.map((item) => item.id);
          let data: UpdateIgnoreAreaDto = {
            ids: allIds,
            ignoreAreas: [newIgnoreArea!],
          };
          testRunService.addIgnoreAreas(data).then(() => {
            setProcessing(false);
            setSelectedRectId(undefined);
            enqueueSnackbar(
              "Ignore areas are updated in all images in this build.",
              {
                variant: "success",
              }
            );
          });
        })
        .catch((error) => {
          enqueueSnackbar("There was an error : " + error, {
            variant: "error",
          });
          setProcessing(false);
        });
    } else {
      enqueueSnackbar(
        "There was an error determining which ignore area to apply.",
        { variant: "error" }
      );
    }
  };

  useHotkeys(
    "d",
    () => !!testRun.diffName && setIsDiffShown((isDiffShown) => !isDiffShown),
    [testRun.diffName]
  );
  useHotkeys("ESC", handleClose, [handleClose]);

  return (
    <React.Fragment>
      <AppBar position="sticky">
        <Toolbar>
          <Grid container justifyContent="space-between">
            <Grid item>
              <Typography variant="h6">{testRun.name}</Typography>
            </Grid>
            {testRun.diffName && (
              <Grid item>
                <Tooltip title={"Hotkey: D"}>
                  <Switch
                    checked={isDiffShown}
                    onChange={() => setIsDiffShown(!isDiffShown)}
                    name="Toggle diff"
                  />
                </Tooltip>
              </Grid>
            )}
            {(testRun.status === TestStatus.unresolved ||
              testRun.status === TestStatus.new) && (
              <Grid item>
                <ApproveRejectButtons testRun={testRun} />
              </Grid>
            )}
            <Grid item>
              <IconButton color="inherit" onClick={handleClose}>
                <Close />
              </IconButton>
            </Grid>
          </Grid>
        </Toolbar>
      </AppBar>
      {processing && <LinearProgress />}
      <Box m={1}>
        <Grid container alignItems="center">
          <Grid item xs={12}>
            <Grid container alignItems="center">
              <Grid item>
                <TestRunDetails testRun={testRun} />
              </Grid>
              {isImageSizeDiffer && (
                <Grid item>
                  <Tooltip
                    title={
                      "Image height/width differ from baseline! Cannot calculate diff!"
                    }
                  >
                    <IconButton>
                      <WarningRounded color="secondary" />
                    </IconButton>
                  </Tooltip>
                </Grid>
              )}
            </Grid>
          </Grid>
          <Grid item>
            <Grid container alignItems="center" spacing={2}>
              <Grid item>
                <Select
                  id="area-select"
                  labelId="areaSelect"
                  value={valueOfIgnoreOrCompare}
                  onChange={(event) =>
                    onIgnoreOrCompareSelectChange(event.target.value as string)
                  }
                >
                  {["Ignore Areas", "Compare Area"].map((eachItem) => (
                    <MenuItem key={eachItem} value={eachItem}>
                      {eachItem}
                    </MenuItem>
                  ))}
                </Select>
              </Grid>
              <Grid item>
                <ToggleButton
                  value={"drawMode"}
                  selected={isDrawMode}
                  onClick={() => {
                    setIsDrawMode(!isDrawMode);
                  }}
                >
                  <Add />
                </ToggleButton>
              </Grid>
              <Grid item>
                <IconButton
                  disabled={!selectedRectId || ignoreAreas.length === 0}
                  onClick={() =>
                    selectedRectId && deleteIgnoreArea(selectedRectId)
                  }
                >
                  <Delete />
                </IconButton>
              </Grid>
              <Tooltip title="Clears all ignore areas." aria-label="reject">
                <Grid item>
                  <IconButton
                    disabled={ignoreAreas.length === 0}
                    onClick={() => {
                      handleIgnoreAreaChange([]);
                    }}
                  >
                    <LayersClear />
                  </IconButton>
                </Grid>
              </Tooltip>
              <Tooltip
                title={applyIgnoreAreaText}
                aria-label="apply ignore area"
              >
                <Grid item>
                  <IconButton
                    disabled={!selectedRectId || ignoreAreas.length === 0}
                    onClick={() => toggleApplyIgnoreDialogOpen()}
                  >
                    <Collections />
                  </IconButton>
                </Grid>
              </Tooltip>
              <Grid item>
                <IconButton
                  disabled={!touched}
                  onClick={() => saveIgnoreAreasOrCompareArea()}
                >
                  <Save />
                </IconButton>
              </Grid>
            </Grid>
          </Grid>
          <Grid item>
            <Button
              color="primary"
              disabled={!testRun.testVariationId}
              onClick={() => {
                navigate(
                  `${routes.VARIATION_DETAILS_PAGE}/${testRun.testVariationId}`
                );
              }}
            >
              Baseline history
            </Button>
          </Grid>
          <Grid item>
            <CommentsPopper
              text={testRun.comment}
              onSave={(comment) =>
                testRunService
                  .update(testRun.id, { comment })
                  .then(() =>
                    enqueueSnackbar("Comment updated", {
                      variant: "success",
                    })
                  )
                  .catch((err) =>
                    enqueueSnackbar(err, {
                      variant: "error",
                    })
                  )
              }
            />
          </Grid>
        </Grid>
      </Box>
      <Box
        overflow="hidden"
        minHeight="65%"
        className={classes.drawAreaContainer}
      >
        <Grid container style={{ height: "100%" }}>
          <Grid item xs={6} className={classes.drawAreaItem}>
            <DrawArea
              type="Baseline"
              imageName={testRun.baselineName}
              branchName={testRun.baselineBranchName}
              imageState={[baselineImage, baselineImageStatus]}
              ignoreAreas={[]}
              tempIgnoreAreas={[]}
              setIgnoreAreas={handleIgnoreAreaChange}
              selectedRectId={selectedRectId}
              setSelectedRectId={setSelectedRectId}
              onStageClick={removeSelection}
              stageScaleState={[stageScale, setStageScale]}
              stagePosState={[stagePos, setStagePos]}
              stageInitPosState={[stageInitPos, setStageInitPos]}
              stageOffsetState={[stageOffset, setStageOffset]}
              drawModeState={[false, setIsDrawMode]}
            />
          </Grid>
          <Grid item xs={6} className={classes.drawAreaItem}>
            {isDiffShown ? (
              <DrawArea
                type="Diff"
                imageName={testRun.diffName}
                branchName={testRun.branchName}
                imageState={[diffImage, diffImageStatus]}
                ignoreAreas={ignoreAreas}
                tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
                setIgnoreAreas={handleIgnoreAreaChange}
                selectedRectId={selectedRectId}
                setSelectedRectId={setSelectedRectId}
                onStageClick={removeSelection}
                stageScaleState={[stageScale, setStageScale]}
                stagePosState={[stagePos, setStagePos]}
                stageInitPosState={[stageInitPos, setStageInitPos]}
                stageOffsetState={[stageOffset, setStageOffset]}
                drawModeState={[isDrawMode, setIsDrawMode]}
              />
            ) : (
              <DrawArea
                type="Image"
                imageName={testRun.imageName}
                branchName={testRun.branchName}
                imageState={[image, imageStatus]}
                ignoreAreas={ignoreAreas}
                tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
                setIgnoreAreas={handleIgnoreAreaChange}
                selectedRectId={selectedRectId}
                setSelectedRectId={setSelectedRectId}
                onStageClick={removeSelection}
                stageScaleState={[stageScale, setStageScale]}
                stagePosState={[stagePos, setStagePos]}
                stageInitPosState={[stageInitPos, setStageInitPos]}
                stageOffsetState={[stageOffset, setStageOffset]}
                drawModeState={[isDrawMode, setIsDrawMode]}
              />
            )}
          </Grid>
        </Grid>
      </Box>
      <ScaleActionsSpeedDial
        onZoomInClick={() => setStageScale(stageScale * stageScaleBy)}
        onZoomOutClick={() => setStageScale(stageScale / stageScaleBy)}
        onOriginalSizeClick={setOriginalSize}
        onFitIntoScreenClick={fitStageToScreen}
      />
      <BaseModal
        open={applyIgnoreDialogOpen}
        title={applyIgnoreAreaText}
        submitButtonText={"Yes"}
        onCancel={toggleApplyIgnoreDialogOpen}
        content={
          <Typography>
            {`All images in the current build will be re-compared with new ignore area taken into account. Are you sure?`}
          </Typography>
        }
        onSubmit={() => {
          toggleApplyIgnoreDialogOpen();
          applyIgnoreArea();
        }}
      />
    </React.Fragment>
  );
}
Example #9
Source File: BulkOperation.tsx    From frontend with Apache License 2.0 4 votes vote down vote up
BulkOperation: React.FunctionComponent = () => {
  const props = useGridSlotComponentProps();
  const { enqueueSnackbar } = useSnackbar();
  const [approveDialogOpen, setApproveDialogOpen] = React.useState(false);
  const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false);
  const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
  const [downloadDialogOpen, setDownloadDialogOpen] = React.useState(false);
  const [clearIgnoreDialogOpen, setClearIgnoreDialogOpen] = React.useState(false);
  const [isProcessing, setIsProcessing] = React.useState(false);
  const ids: GridRowId[] = React.useMemo(
    () => Object.values(props.state.selection),
    [props.state.selection]
  );
  const isMerge: boolean = React.useMemo(
    () =>
      !!head(
        props.rows.filter((value: GridRowData) =>
          ids.includes(value.id.toString())
        )
      )?.merge,
    // eslint-disable-next-line
    [ids]
  );
  const idsEligibleForApproveOrReject: string[] = React.useMemo(
    () =>
      props.rows
        .filter(
          (value: GridRowData) =>
            ids.includes(value.id.toString()) &&
            [TestStatus.new, TestStatus.unresolved].includes(
              value.status.toString()
            )
        )
        .map((value: GridRowData) => value.id.toString()),
    // eslint-disable-next-line
    [ids]
  );

  const selectedRows: GridSelectionModel = props.state.selection;
  const count = Object.keys(selectedRows).length;

  const toggleApproveDialogOpen = () => {
    setApproveDialogOpen(!approveDialogOpen);
  };
  const toggleRejectDialogOpen = () => {
    setRejectDialogOpen(!rejectDialogOpen);
  };
  const toggleDeleteDialogOpen = () => {
    setDeleteDialogOpen(!deleteDialogOpen);
  };
  const toggleDownloadDialogOpen = () => {
    setDownloadDialogOpen(!downloadDialogOpen);
  };
  const toggleClearIgnoreDialogOpen = () => {
    setClearIgnoreDialogOpen(!clearIgnoreDialogOpen);
  };

  const getTitle = () => {
    if (clearIgnoreDialogOpen) {
      return "Clear Ignore Area For Selected Items";
    }
    return submitButtonText() + " Test Runs";
  };

  const submitButtonText = (): string => {
    if (approveDialogOpen) {
      return "Approve";
    }
    if (rejectDialogOpen) {
      return "Reject";
    }
    if (deleteDialogOpen) {
      return "Delete";
    }
    if (downloadDialogOpen) {
      return "Download";
    }
    if (clearIgnoreDialogOpen) {
      return "Clear";
    }
    return "";
  };

  const closeModal = () => {
    if (deleteDialogOpen) {
      return toggleDeleteDialogOpen();
    }
    if (downloadDialogOpen) {
      return toggleDownloadDialogOpen();
    }
    if (approveDialogOpen) {
      return toggleApproveDialogOpen();
    }
    if (rejectDialogOpen) {
      return toggleRejectDialogOpen();
    }
    if (clearIgnoreDialogOpen) {
      return toggleClearIgnoreDialogOpen();
    }
  };

  const getBulkAction = () => {
    if (deleteDialogOpen) {
      return testRunService.removeBulk(ids);
    }
    if (downloadDialogOpen) {
      let urlsToDownload: { download: string, filename: string }[] = [];
      ids.forEach((id) => {
        testRunService.getDetails(id.toString())
          .then(
            (e) => {
              urlsToDownload.push({ "download": "static/imageUploads/" + e.imageName, "filename": e.name });
              //Call getFile function only when all images names are pushed into the array.
              if (urlsToDownload.length === ids.length) {
                testRunService.getFiles(urlsToDownload);
              }
            });
      });
    }
    if (rejectDialogOpen) {
      return testRunService.rejectBulk(idsEligibleForApproveOrReject);
    }
    if (approveDialogOpen) {
      return testRunService.approveBulk(idsEligibleForApproveOrReject, isMerge);
    }
    return testRunService.updateIgnoreAreas({
      ids,
      ignoreAreas: [],
    });
  };

  const dismissDialog = () => {
    if (deleteDialogOpen) {
      return toggleDeleteDialogOpen();
    }
    if (downloadDialogOpen) {
      return toggleDownloadDialogOpen();
    }
    if (approveDialogOpen) {
      return toggleApproveDialogOpen();
    }
    if (clearIgnoreDialogOpen) {
      return toggleClearIgnoreDialogOpen();
    }
    return toggleRejectDialogOpen();
  };

  return (
    <>
      <Tooltip
        title="Approve unresolved in selected rows."
        aria-label="approve"
      >
        <span>
          <IconButton disabled={count === 0} onClick={toggleApproveDialogOpen}>
            <ThumbUp />
          </IconButton>
        </span>
      </Tooltip>
      <Tooltip title="Reject unresolved in selected rows." aria-label="reject">
        <span>
          <IconButton disabled={count === 0} onClick={toggleRejectDialogOpen}>
            <ThumbDown />
          </IconButton>
        </span>
      </Tooltip>
      <Tooltip title="Download images for selected rows." aria-label="download">
        <span>
          <IconButton disabled={count === 0} onClick={toggleDownloadDialogOpen}>
            <CloudDownload />
          </IconButton>
        </span>
      </Tooltip>
      <Tooltip title="Delete selected rows." aria-label="delete">
        <span>
          <IconButton disabled={count === 0} onClick={toggleDeleteDialogOpen}>
            <Delete />
          </IconButton>
        </span>
      </Tooltip>
      <Tooltip
        title="Clear ignore areas for selected rows."
        aria-label="clear ignore area"
      >
        <span>
          <IconButton
            disabled={count === 0}
            onClick={toggleClearIgnoreDialogOpen}
          >
            <LayersClear />
          </IconButton>
        </span>
      </Tooltip>

      <BaseModal
        open={
          deleteDialogOpen ||
          downloadDialogOpen ||
          approveDialogOpen ||
          rejectDialogOpen ||
          clearIgnoreDialogOpen
        }
        title={getTitle()}
        submitButtonText={submitButtonText()}
        onCancel={dismissDialog}
        content={
          <Typography>
            {`Are you sure you want to ${submitButtonText().toLowerCase()} ${count} items?`}
          </Typography>
        }
        onSubmit={() => {
          setIsProcessing(true);
          getBulkAction()
            .then(() => {
              setIsProcessing(false);
              enqueueSnackbar(`${count} test runs processed.`, {
                variant: "success",
              });
            })
            .catch((err) => {
              setIsProcessing(false);
              enqueueSnackbar(err, {
                variant: "error",
              });
            });
          closeModal();
        }}
      />
      {isProcessing && <LinearProgress />}
    </>
  );
}
Example #10
Source File: ChartWithTooltip.tsx    From aqualink-app with MIT License 4 votes vote down vote up
function ChartWithTooltip({
  chartSettings,
  children,
  className,
  style,
  ...rest
}: PropsWithChildren<ChartWithTooltipProps>) {
  const { siteId, surveys, timeZone, startDate, endDate, datasets } = rest;
  const chartDataRef = useRef<Line>(null);

  const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
  const [tooltipData, setTooltipData] = useState<TooltipData>({
    siteId,
    date: "",
    datasets: [],
    surveyId: null,
  });
  const [showTooltip, setShowTooltip] = useState<boolean>(false);

  const customTooltip =
    (ref: React.RefObject<Line>) => (tooltipModel: ChartTooltipModel) => {
      const chart = ref.current;
      if (!chart?.chartInstance.canvas) {
        return;
      }

      const date = tooltipModel.dataPoints?.[0]?.xLabel;
      if (typeof date !== "string") return;

      const dateObject = new Date(date);

      const surveyId = findSurveyFromDate(date, surveys);

      const datasetsDates = getDatasetsTimestamps(datasets);
      const minDataDate = minBy(datasetsDates, (item) => new Date(item));
      const maxDataDate = maxBy(datasetsDates, (item) => new Date(item));

      const closestDatasetData = getTooltipClosestData(dateObject, datasets);

      const nValues = closestDatasetData
        .map(({ data }) => head(data)?.value)
        .filter(isNumber).length;

      // Chart.js displays tooltips in a parallel to the X axis preference, meaning
      // that it will appear right or left from the chart point. We want to change that,
      // and display the tooltip in a Y axis preference, and more specifically, above the chart point.
      const position = chart.chartInstance.canvas.getBoundingClientRect();

      // We center the tooltip in the X axis by subtracting half its width.
      const left = position.left + tooltipModel.caretX - TOOLTIP_WIDTH / 2;

      // We increase the tooltip's top, so that it lands above the chart point. The amount by
      // which we increase varies based on how many values we display and if there is a survey at that point,
      // as we display a `VIEW SURVEY` button.
      const top =
        position.top +
        tooltipModel.caretY -
        ((surveyId ? 30 : 0) + nValues * 20 + 50);

      // We display the tooltip only if there are data to display at this point and it lands
      // between the chart's X axis limits.
      if (
        nValues > 0 &&
        moment(date).isBetween(
          moment(startDate || minDataDate),
          moment(endDate || maxDataDate),
          undefined,
          "[]"
        )
      ) {
        setTooltipPosition({ top, left });
        setTooltipData({
          ...tooltipData,
          date,
          surveyId,
          datasets: closestDatasetData,
        });
        setShowTooltip(true);
      }
    };

  const hideTooltip = () => {
    setShowTooltip(false);
  };

  // Hide tooltip on scroll to avoid dragging it on the page.
  if (showTooltip) {
    window.addEventListener("scroll", hideTooltip);
  }

  return (
    <div className={className} style={style} onMouseLeave={hideTooltip}>
      {children}
      <Chart
        {...rest}
        chartRef={chartDataRef}
        chartSettings={{
          tooltips: {
            enabled: false,
            intersect: false,
            custom: customTooltip(chartDataRef),
          },
          legend: {
            display: false,
          },
          // we could use mergeWith here too, but currently nothing would use it.
          ...chartSettings,
        }}
      />
      {showTooltip ? (
        <div
          className="chart-tooltip"
          id="chart-tooltip"
          style={{
            position: "fixed",
            top: tooltipPosition.top,
            left: tooltipPosition.left,
          }}
        >
          <Tooltip
            {...tooltipData}
            siteTimeZone={timeZone}
            userTimeZone={Intl.DateTimeFormat().resolvedOptions().timeZone}
          />
        </div>
      ) : null}
    </div>
  );
}
Example #11
Source File: index.tsx    From aqualink-app with MIT License 4 votes vote down vote up
WaterSamplingCard = ({ siteId }: WaterSamplingCardProps) => {
  const classes = useStyles();
  const [minDate, setMinDate] = useState<string>();
  const [maxDate, setMaxDate] = useState<string>();
  const [point, setPoint] = useState<SurveyPoint>();
  const [timeSeriesData, setTimeSeriesData] = useState<TimeSeriesData>();
  const meanValues = calculateSondeDataMeanValues(METRICS, timeSeriesData);
  const isPointNameLong = (point?.name?.length || 0) > 24;
  const surveyPointDisplayName = `${isPointNameLong ? "" : " Survey point:"} ${
    point?.name || point?.id
  }`;
  const viewUploadButtonLink = `/sites/${siteId}${requests.generateUrlQueryParams(
    {
      start: minDate,
      end: maxDate,
      surveyPoint: point?.id,
    }
  )}`;
  const lastUpload = maxDate ? moment(maxDate).format("MM/DD/YYYY") : undefined;

  useEffect(() => {
    const getCardData = async () => {
      try {
        const { data: uploadHistory } = await siteServices.getSiteUploadHistory(
          parseInt(siteId, 10)
        );
        // Upload history is sorted by `maxDate`, so the first
        // item is the most recent.
        const {
          minDate: from,
          maxDate: to,
          surveyPoint,
        } = head(uploadHistory) || {};
        if (typeof surveyPoint?.id === "number") {
          const [data] = await timeSeriesRequest({
            siteId,
            pointId: surveyPoint.id.toString(),
            start: from,
            end: to,
            metrics: METRICS,
            hourly: true,
          });
          setMinDate(from);
          setMaxDate(to);
          setTimeSeriesData(data);
          setPoint(surveyPoint);
        }
      } catch (err) {
        console.error(err);
      }
    };

    getCardData();
  }, [siteId]);

  return (
    <Card className={classes.root}>
      <CardHeader
        className={classes.header}
        title={
          <Grid container>
            <Grid item>
              <Typography className={classes.cardTitle} variant="h6">
                WATER SAMPLING
              </Typography>
            </Grid>
          </Grid>
        }
      />

      <CardContent className={classes.content}>
        <Box p="1rem" display="flex" flexGrow={1}>
          <Grid container spacing={1}>
            {metrics(meanValues).map(({ label, value, unit, xs }) => (
              <Grid key={label} item xs={xs}>
                <Typography
                  className={classes.contentTextTitles}
                  variant="subtitle2"
                >
                  {label}
                </Typography>
                <Typography
                  className={classes.contentTextValues}
                  variant="h3"
                  display="inline"
                >
                  {value}
                </Typography>
                <Typography
                  className={classes.contentUnits}
                  display="inline"
                  variant="h6"
                >
                  {unit}
                </Typography>
              </Grid>
            ))}
          </Grid>
        </Box>
        <UpdateInfo
          relativeTime={lastUpload}
          chipWidth={64}
          timeText="Last data uploaded"
          imageText="VIEW UPLOAD"
          href={viewUploadButtonLink}
          subtitle={point && surveyPointDisplayName}
        />
      </CardContent>
    </Card>
  );
}
Example #12
Source File: addon-card-list.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
AddonCardList = (props: IProps) => {
  const [dataSource, setDataSource] = React.useState([] as any[]);
  const [searchKey, setSearchKey] = React.useState('');
  const [name, setName] = React.useState('');
  const [dataSourceType, setDataSourceType] = React.useState('ALL');
  const [env, setEnv] = React.useState('ALL');
  const [activeCategory, setActiveCategory] = React.useState();
  const categoryRefs = React.useRef([] as any[]);
  const categoryCardsRef = React.useRef(null);
  const { searchPlaceHolder, addonCategory, isFetching, onEitAddon, isPlatform } = props;

  const debounceCheck = React.useCallback(
    debounce(() => {
      const categoryList = categoryCardsRef.current as any;
      categoryRefs.current.forEach((categorySection: any) => {
        const { current } = head(Object.values(categorySection)) as any;
        if (current) {
          const top = current.offsetTop - categoryList.scrollTop - 52;
          const newActiveCategory = head(Object.keys(categorySection));
          if (top <= 0) {
            setActiveCategory(newActiveCategory);
          }
        }
      });
    }, 100),
    [],
  );

  const operateScrollEvent = (isRemove?: boolean) => {
    const categoryList = categoryCardsRef.current as any;
    if (categoryList) {
      !isRemove
        ? categoryList.addEventListener('scroll', debounceCheck)
        : categoryList.removeEventListener('scroll', debounceCheck);
    }
  };

  const scrollToTarget = (targetCategoryName: string) => {
    const targetRef = categoryRefs.current.find((ref: any) => ref[targetCategoryName]);
    const targetCategoryDom = targetRef[targetCategoryName].current;
    if (!targetCategoryDom) {
      return;
    }
    targetCategoryDom.parentNode.scrollTop = targetCategoryDom.offsetTop - 20;
  };

  React.useEffect(() => {
    if (!isEmpty(props.addonCategory) && !isEmpty(dataSource)) {
      const firstCategory = head(dataSource) as any[];
      const targetCategory = head(firstCategory);
      scrollToTarget(targetCategory);
    }
  }, [dataSource, props.addonCategory]);

  React.useEffect(() => {
    operateScrollEvent();
    return () => {
      operateScrollEvent(true);
    };
  });
  useUpdateEffect(() => {
    const { addonList = [], searchProps } = props;
    if (!isEmpty(addonList)) {
      setDataSource(
        addonList.filter((addon: ADDON.Instance) => {
          const isOtherFilter =
            (addon.workspace === env || env === 'ALL') &&
            searchFilter(addon, searchKey, searchProps) &&
            searchFilter(addon, name, searchProps);

          if (dataSourceType === 'custom') {
            const isCustomDataSource = CustomDataSourceMap.includes(addon.displayName);
            return isOtherFilter && isCustomDataSource;
          }
          return isOtherFilter && (addon.displayName?.toLowerCase() === dataSourceType || dataSourceType === 'ALL');
        }),
      );
    } else if (!isEmpty(addonCategory)) {
      const cloneAddonCategory = cloneDeep(addonCategory);
      Object.keys(cloneAddonCategory).forEach((key: string) => {
        const value = cloneAddonCategory[key];
        cloneAddonCategory[key] = value.filter(
          (addon: IAddon) => (addon.workspace === env || env === 'ALL') && searchFilter(addon, searchKey, searchProps),
        );
      });
      const filterDataSource = Object.entries(pickBy(cloneAddonCategory, (x: any[]) => x.length > 0));
      const resolvedFilterDataSource: any[] = [];
      const categories = Object.keys(addonCategory);
      forEach(categories, (v) => {
        const targetItem = find(filterDataSource, (item) => item[0] === v);
        targetItem && resolvedFilterDataSource.push(targetItem);
      });
      setDataSource(resolvedFilterDataSource);
      categoryRefs.current = Object.keys(cloneAddonCategory).map((key: string) => {
        return { [key]: React.createRef() };
      });
      setActiveCategory(head(head(resolvedFilterDataSource)));
    } else {
      setDataSource([]);
    }
  }, [env, searchKey, props.addonList, props.addonCategory, props, name, dataSourceType]);

  const onSearchKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = get(event, 'target.value');
    setSearchKey(value);
  };

  const onNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = get(event, 'target.value');
    setName(value);
  };

  const onEnvChange = (value: string) => {
    setEnv(value);
  };

  const onDataSourceChange = (value: any) => {
    setDataSourceType(value);
  };

  const onClickCategory = (event: React.MouseEvent) => {
    const targetCategory = event.currentTarget.getAttribute('value') as string;
    if (activeCategory === targetCategory) {
      return;
    }
    setActiveCategory(targetCategory);
    scrollToTarget(targetCategory);
  };

  const renderCategoryList = () => {
    if (addonCategory) {
      const categories = Object.keys(addonCategory);
      const resolvedCategories = categories.filter((v) => CategoriesOrder.includes(v));
      return resolvedCategories.map((key: string) => {
        key = allWordsFirstLetterUpper(key);
        return (
          <li
            key={key}
            className={`category-item cursor-pointer ${activeCategory === key ? 'active' : ''}`}
            value={key}
            onClick={onClickCategory}
          >
            <Tooltip title={key}>{key}</Tooltip>
          </li>
        );
      });
    }
    return null;
  };

  return (
    <section className="addon-card-list">
      <Spin wrapperClassName="full-spin-height" spinning={isFetching}>
        <div className="addon-filter">
          {!props.showDataSourceSearch && (
            <Select className="env-select" defaultValue="ALL" onChange={onEnvChange}>
              {envOptions}
            </Select>
          )}
          {props.hideSearch ? null : (
            <Search className="data-select" onChange={onSearchKeyChange} placeholder={searchPlaceHolder} />
          )}
          {props.showDataSourceSearch && (
            <Search className="data-select mr-5" onChange={onNameChange} placeholder={searchPlaceHolder} />
          )}
          {props.showDataSourceSelect && (
            <Select className="data-select" defaultValue="ALL" onChange={onDataSourceChange}>
              {dataSourceTypeOptions}
            </Select>
          )}
        </div>
        <div className="addons-content">
          <IF check={!isEmpty(addonCategory)}>
            <div className="addon-menu">
              <span className="content-title font-medium">{firstCharToUpper(i18n.t('dop:addon category'))}</span>
              <ul className="menu-list">{renderCategoryList()}</ul>
            </div>
          </IF>
          <AddonCards
            forwardedRef={categoryCardsRef}
            dataSource={dataSource}
            onEitAddon={onEitAddon}
            isPlatform={isPlatform}
            categoryRefs={categoryRefs.current}
          />
        </div>
      </Spin>
    </section>
  );
}
Example #13
Source File: edit-stage.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
EditStage = (props: IEditStageProps & FormProps) => {
  const [form] = Form.useForm();
  const [state, updater] = useUpdate({
    task: {} as IStageTask | {},
    actionConfig: {} as DEPLOY.ActionConfig | {},
    resource: {},
    originType: null as null | string,
    originName: null as null | string,
  });

  const initialValue = React.useRef({});

  const { task, actionConfig, resource, originName, originType } = state;
  const { actions, otherTaskAlias, editing, isCreateTask, onSubmit: handleSubmit, task: PropsTask } = props;
  const { getFieldValue } = form;

  const actionConfigs = deployStore.useStore((s) => s.actionConfigs);
  const { getActionConfigs } = deployStore.effects;
  const [loading] = useLoading(deployStore, ['getActionConfigs']);
  React.useEffect(() => {
    if (!isEmpty(PropsTask)) {
      updater.originName(PropsTask.alias);
      updater.originType(PropsTask.type);
      updater.task(PropsTask);
    }
  }, [PropsTask, updater]);

  React.useEffect(() => {
    let config;
    if (actionConfigs.length > 0) {
      config = PropsTask.version
        ? actionConfigs.find((c) => c.version === PropsTask.version)
        : getDefaultVersionConfig(actionConfigs);
    }

    const newResource = getResource(PropsTask, config);
    updater.resource(newResource);
    updater.actionConfig(config as DEPLOY.ActionConfig);
  }, [actionConfigs, PropsTask, updater]);

  React.useEffect(() => {
    if (isCreateTask) {
      updater.actionConfig({});
    }
  }, [isCreateTask, updater]);

  if (!isCreateTask && isEmpty(actionConfig)) {
    return null;
  }

  const type = actionConfig.type || getFieldValue(['resource', 'type']);
  const taskInitName =
    originType === actionConfig.name
      ? originName
      : otherTaskAlias.includes(actionConfig.name)
      ? undefined
      : actionConfig.name;

  const changeResourceType = (value: string) => {
    const action = actions.find((a: any) => a.name === value);
    if (action) {
      getActionConfigs({ actionType: action.name }).then((result: DEPLOY.ActionConfig[]) => {
        const config = getDefaultVersionConfig(result);
        const mergedResource = mergeActionAndResource(config, {} as any);
        updater.resource({
          ...resource,
          ...mergedResource,
        });
      });
    }
  };

  const checkResourceName = (_rule: any, value: string, callback: any) => {
    const name = form.getFieldValue(['resource', 'alias']);

    if (!value) {
      return callback(i18n.t('dop:please enter the task name'));
    }
    if (otherTaskAlias.includes(name)) {
      return callback(i18n.t('dop:An Action with the same name exists.'));
    }
    callback();
  };

  const changeActionVersion = (version: string) => {
    const selectConfig = actionConfigs.find((config) => config.version === version) as DEPLOY.ActionConfig;
    updater.actionConfig(selectConfig);
    updater.resource(getResource(task, selectConfig));
  };

  const taskType = (
    <Item
      name="resource.type"
      initialValue={task.type}
      rules={[
        {
          required: true,
          message: `${i18n.t('dop:Please select')}Task Type`,
        },
      ]}
    >
      <ActionSelect
        disabled={!editing}
        label={i18n.t('Task type')}
        actions={actions}
        onChange={changeResourceType}
        placeholder={`${i18n.t('dop:Please choose the task type')}`}
      />
    </Item>
  );

  const actionVersion = (
    <Item
      label="version"
      name="resource.version"
      initialValue={task.version || actionConfig.version}
      rules={[
        {
          required: true,
          message: `${i18n.t('dop:Please select')}Task Version`,
        },
      ]}
    >
      <Select disabled={!editing} onChange={changeActionVersion} placeholder={`${i18n.t('dop:please choose version')}`}>
        {actionConfigs.map((config) => (
          <Option key={config.version} value={config.version}>
            {config.version}
          </Option>
        ))}
      </Select>
    </Item>
  );

  let alert;

  if (!isCreateTask && isEmpty(resource)) {
    return null;
  }

  if (!isCreateTask && !actionConfig.type) {
    alert = (
      <ErdaAlert
        className="addon-error-tag"
        message={i18n.t('dop:the current action does not exist, please re-select!')}
        type="error"
      />
    );
  }
  const taskName = (
    <Item
      label={i18n.t('dop:task name')}
      name="resource.alias"
      initialValue={taskInitName}
      rules={[
        {
          required: true,
          validator: checkResourceName,
        },
      ]}
    >
      <Input autoFocus={!type} disabled={!editing} placeholder={i18n.t('dop:please enter the task name')} />
    </Item>
  );

  const renderTaskTypeStructure = () => {
    if (isEmpty(resource)) {
      return null;
    }
    const { getFieldsValue } = form;
    const resourceForm = getFieldsValue(['resource.alias', 'resource.type']);
    if (!resourceForm.resource.type) {
      return null;
    }

    return renderResource(resource, 'resource');
  };

  const getDataValue = (dataSource: any, key: string) => {
    return dataSource ? dataSource[key] : null;
  };

  const renderResource = (resourceParam: any, parentKey?: string, dataSource?: any) => {
    if (resourceParam.data instanceof Array) {
      return resourceParam.data.map((item: any) => {
        const inputKey = parentKey ? `${parentKey}.${item.name}` : `${item.name}`;
        return renderObject(item, inputKey, getDataValue(dataSource, item.name));
      });
    }
    const { params, image, resources } = resourceParam.data;

    const parentObjectData = getDataValue(dataSource, 'params');
    const paramsContent = map(params, (value: any, itemKey: string) => {
      const inputKey = parentKey ? `${parentKey}.params.${itemKey}` : `params.${itemKey}`;
      return renderObject(value, inputKey, getDataValue(parentObjectData, itemKey));
    });

    return (
      <>
        {actionConfig.name === 'custom-script' ? (
          <div>{renderObject(image, 'resource.image', getDataValue(dataSource, 'image'))}</div>
        ) : null}
        <div>
          <div className="resource-input-group-title">params: </div>
          {paramsContent}
        </div>
        <div>{renderObject(resources, 'resource.resources', getDataValue(dataSource, 'resources'))}</div>
      </>
    );
  };

  const renderObject = (value: any, parentKey: string, dataSource?: any) => {
    if (!isObject(value.type)) {
      return renderPropertyValue(value, parentKey, dataSource);
    }

    if (value.type === 'string_array') {
      return renderStringArray(value, parentKey);
    }

    if (value.type === 'struct_array') {
      return renderStructArray(value, parentKey);
    }

    if (value.type === 'map') {
      return renderMap(value, parentKey, dataSource);
    }

    const content = renderResource({ data: value.struct }, parentKey, dataSource);
    if (!content || !Object.values(content).some((c) => c)) return null;

    return (
      <div key={parentKey}>
        <span className="resource-input-group-title">{value.name}: </span>
        <div>{content}</div>
      </div>
    );
  };

  const renderMap = (value: any, parentKey: string, dataSource?: any) => {
    let initialValue = isCreateTask ? value.default : value.value || value.default;

    if (dataSource) {
      initialValue = dataSource;
    }

    if (!editing && !initialValue) {
      return null;
    }

    const inputField = (
      <Item
        key={parentKey}
        name={parentKey}
        initialValue
        rules={[
          {
            required: value.required,
            message: i18n.t('dop:this item cannot be empty'),
          },
        ]}
      >
        {renderTooltip(value.desc, <VariableInput disabled={!editing} label={value.name} />)}
      </Item>
    );
    return inputField;
  };

  const renderStringArray = (value: any, parentKey: string) => {
    const inputField = (
      <Item
        key={parentKey}
        name={parentKey}
        initialValue={isCreateTask ? value.default : value.value || value.default}
        rules={[
          {
            required: value.required,
            message: i18n.t('dop:this item cannot be empty'),
          },
        ]}
      >
        {renderTooltip(value.desc, <ListInput disabled={!editing} label={value.name} />)}
      </Item>
    );
    return inputField;
  };

  const renderPropertyValue = (value: any, parentKey: string, dataSource?: any) => {
    let input;
    let initialValue = isCreateTask ? value.default : value.value || value.default;

    if (dataSource) {
      initialValue = dataSource;
    }

    if (!editing && !initialValue) {
      return null;
    }

    const unit = value.unit ? <span>{value.unit}</span> : null;

    switch (value.type) {
      case 'float':
      case 'int':
        input = (
          <InputNumber
            disabled={!editing || value.readOnly}
            className="w-full"
            placeholder={i18n.t('dop:please enter data')}
          />
        );
        break;
      default:
        input = (
          <Input
            disabled={!editing || value.readOnly}
            placeholder={i18n.t('dop:please enter data')}
            addonAfter={unit}
          />
        );
        break;
    }

    const inputField = (
      <Item
        key={parentKey}
        label={value.name}
        name={parentKey}
        initialValue
        rules={[
          {
            required: value.required,
            message: i18n.t('dop:this item cannot be empty'),
          },
        ]}
      >
        {renderTooltip(value.desc, input)}
      </Item>
    );
    return inputField;
  };

  const renderStructArray = (property: any, parentKey: string) => {
    if ((!editing && !property.value) || (!editing && property.value && !property.value.length)) {
      return null;
    }
    const addBtn = editing ? (
      <ErdaIcon
        type="plus"
        className="cursor-pointer"
        onClick={() => addNewItemToStructArray(property.value, property.struct[0])}
      />
    ) : null;
    initialValue.current = {
      [`${parentKey}-data`]: property.value || [],
    };
    const data = getFieldValue(`${parentKey}-data`);
    const content = data.map((item: any, index: number) => {
      const keys = Object.keys(item);
      const header = (
        <div>
          <span>{typeof item[keys[0]] === 'string' ? item[keys[0]] : 'module'}</span>
          {editing ? (
            <CustomIcon
              onClick={() => deleteItemFromStructArray(index, parentKey)}
              className="icon-delete"
              type="sc1"
            />
          ) : null}
        </div>
      );
      return (
        <Panel key={`${parentKey}.${item.name}`} header={header}>
          {renderResource({ data: property.struct }, `${parentKey}[${index}]`, item)}
        </Panel>
      );
    });

    return (
      <div key={parentKey}>
        <span className="resource-input-group-title">
          {property.name}:{addBtn}
        </span>
        {data.length ? (
          <Collapse className="collapse-field" accordion>
            {content}
          </Collapse>
        ) : null}
      </div>
    );
  };

  const deleteItemFromStructArray = (index: number, parentKey: string) => {
    const formDatas = form.getFieldValue(`${parentKey}-data`);
    formDatas.splice(index, 1);

    form.setFieldsValue({
      [parentKey]: formDatas,
    });
  };

  const addNewItemToStructArray = (list: any[], struct: any) => {
    list.push({
      [struct.name]: `module-${list.length + 1}`,
    });
    updater.resource(cloneDeep(resource));
  };

  const isObject = (inputType: string) => {
    return ['map', 'string_array', 'struct_array', 'struct'].includes(inputType);
  };

  const renderTooltip = (message: string, text: any) => {
    if (!message) {
      return text;
    }
    const msgComp = <pre className="prop-popover">{message}</pre>;
    return (
      <Popover placement="leftTop" trigger={['focus']} content={msgComp}>
        {text}
      </Popover>
    );
  };

  const onSubmit = () => {
    form
      .validateFields()
      .then((values: any) => {
        let data = cloneDeep(values);
        const resources = head(filter(state.resource.data, (item) => item.name === 'resources'));
        const originResource = transform(
          get(resources, 'struct'),
          (result, item: { name: string; default: string | number }) => {
            const { name, default: d } = item;
            // eslint-disable-next-line no-param-reassign
            result[name] = +d;
          },
          {},
        );
        const editedResources = get(data, 'resource.resources');
        forEach(Object.entries(editedResources), ([key, value]) => {
          editedResources[key] = +(value as string);
        });
        const isResourceDefault = isEqual(editedResources, originResource);

        if (isResourceDefault) {
          data = omit(data, ['resource.resources']);
        }
        const filledFieldsData = clearEmptyField(data);
        handleSubmit(filledFieldsData);
      })
      .catch(({ errorFields }: { errorFields: Array<{ name: any[]; errors: any[] }> }) => {
        form.scrollToField(errorFields[0].name);
      });
  };

  const clearEmptyField = (ObjData: any) => {
    const filledFields: string[] = [];
    const findData = (obj: any, parentArray: string[]) => {
      Object.keys(obj).forEach((key) => {
        const currentParent = [...parentArray, key];
        const value = get(obj, key);
        if (typeof value === 'object') {
          findData(value, currentParent);
        } else if (value || value === 0) {
          filledFields.push(currentParent.join('.'));
        }
      });
    };
    findData(ObjData, []);
    return pick(ObjData, filledFields);
  };

  return (
    <Spin spinning={loading}>
      <Form form={form} initialValues={initialValue.current} className="edit-service-container">
        {alert}
        {taskType}
        {type ? taskName : null}
        {actionVersion}
        {renderTaskTypeStructure()}
        {editing ? (
          <Button type="primary" ghost onClick={onSubmit}>
            {i18n.t('Save')}
          </Button>
        ) : null}
      </Form>
    </Spin>
  );
}
Example #14
Source File: pipeline-node-drawer.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
PurePipelineNodeForm = (props: IEditStageProps & FormProps) => {
  const [form] = Form.useForm();
  const {
    nodeData: propsNodeData,
    editing,
    isCreate,
    otherTaskAlias = [],
    onSubmit: handleSubmit = noop,
    chosenActionName,
    chosenAction,
    actionSpec,
  } = props;
  const [actionConfigs] = appDeployStore.useStore((s) => [s.actionConfigs]);

  const { getFieldValue } = form;
  const [{ actionConfig, resource, originType, originName, task, changeKey }, updater, update] = useUpdate({
    resource: {},
    actionConfig: {} as DEPLOY.ActionConfig,
    originType: null as null | string,
    originName: null as null | string,
    task: {} as IStageTask,
    changeKey: 0,
  });

  useEffectOnce(() => {
    handleActionSpec();
  });

  React.useEffect(() => {
    if (propsNodeData && !isEmpty(propsNodeData)) {
      update({
        originName: propsNodeData.alias,
        originType: propsNodeData.type,
        task: propsNodeData,
      });
    }
  }, [propsNodeData, update]);

  React.useEffect(() => {
    if (isCreate) {
      updater.actionConfig({} as DEPLOY.ActionConfig);
    }
  }, [isCreate, updater]);

  const taskInitName =
    originType === actionConfig.name
      ? originName
      : otherTaskAlias.includes(actionConfig.name)
      ? undefined
      : actionConfig.name;

  const taskInitVersion = task.version || actionConfig.version;
  useUpdateEffect(() => {
    const prevResource = form.getFieldValue('resource') || {};
    form.setFieldsValue({
      resource: {
        ...prevResource,
        type: chosenActionName,
        alias: taskInitName,
        version: taskInitVersion,
      },
    });
    updater.changeKey((prev: number) => prev + 1);
  }, [taskInitName, taskInitVersion, chosenActionName]);

  const handleActionSpec = () => {
    let _config;
    let _resource;
    if (propsNodeData && !isEmpty(propsNodeData)) {
      if (actionSpec.length > 0) {
        _config = propsNodeData.version
          ? actionSpec.find((c) => c.version === propsNodeData.version)
          : getDefaultVersionConfig(actionSpec);
      }
      _resource = getResource(propsNodeData, _config);
    } else {
      _config = getDefaultVersionConfig(actionSpec);
      const mergedResource = mergeActionAndResource(_config, {});
      _resource = { ...resource, ...mergedResource };
    }
    update({
      resource: _resource,
      actionConfig: _config || ({} as DEPLOY.ActionConfig),
    });
  };

  useUpdateEffect(() => {
    handleActionSpec();
  }, [actionSpec]);

  if (!isCreate && isEmpty(actionConfig)) {
    return null;
  }
  const type = actionConfig.type || getFieldValue(['resource', 'type']);

  const checkResourceName = (_rule: any, value: string, callback: any) => {
    const name = form.getFieldValue(['resource', 'alias']);

    if (!value) {
      return callback(i18n.t('dop:please enter the task name'));
    }
    if (otherTaskAlias.includes(name)) {
      return callback(i18n.t('dop:An Action with the same name exists.'));
    }
    callback();
  };

  const changeActionVersion = (version: any) => {
    const selectConfig = actionConfigs.find((config) => config.version === version) as DEPLOY.ActionConfig;
    updater.actionConfig(selectConfig);
    updater.resource(getResource(task as IStageTask, selectConfig));
  };

  const taskType = (
    <Item
      className="hidden"
      name={['resource', 'type']}
      initialValue={chosenActionName}
      rules={[
        {
          required: true,
          message: `${i18n.t('dop:Please select')}Task Type`,
        },
      ]}
    >
      <Input />
    </Item>
  );

  const loopData = (
    <Item className="hidden" name={['resource', 'loop']} initialValue={get(actionConfig, 'spec.loop')} />
  );

  const actionVersion = (
    <Item
      label={i18nMap.version}
      name={['resource', 'version']}
      initialValue={task.version || actionConfig.version}
      rules={[
        {
          required: true,
          message: `${i18n.t('dop:Please select')}Task Version`,
        },
      ]}
    >
      <Select disabled={!editing} onChange={changeActionVersion} placeholder={`${i18n.t('dop:please choose version')}`}>
        {actionConfigs.map((config) => (
          <Option key={config.version} value={config.version}>
            {config.version}
          </Option>
        ))}
      </Select>
    </Item>
  );

  let alert;
  if (!isCreate && !actionConfig.type) {
    alert = (
      <ErdaAlert
        className="addon-error-tag"
        showIcon
        message={i18n.t('dop:the current action does not exist, please re-select!')}
        type="error"
      />
    );
  }

  const taskName = (
    <Item
      label={i18n.t('dop:task name')}
      name={['resource', 'alias']}
      initialValue={taskInitName}
      rules={[
        {
          required: true,
          validator: checkResourceName,
        },
      ]}
    >
      <Input autoFocus={!type} disabled={!editing} placeholder={i18n.t('dop:please enter the task name')} />
    </Item>
  );

  const renderTaskTypeStructure = () => {
    if (isEmpty(resource)) {
      return null;
    }

    const { getFieldsValue } = form;
    const resourceForm = getFieldsValue([
      ['resource', 'alias'],
      ['resource', 'type'],
    ]);
    if (!get(resourceForm, 'resource.type')) {
      return null;
    }

    return renderResource(resource, 'resource');
  };

  const getDataValue = (dataSource: any, key: string) => {
    return dataSource ? dataSource[key] : null;
  };

  const renderResource = (resourceParam: any, parentKey?: string, dataSource?: any) => {
    if (resourceParam.data instanceof Array) {
      return resourceParam.data.map((item: any) => {
        const inputKey = parentKey ? `${parentKey}.${item.name}` : `${item.name}`;
        return renderObject(item, inputKey, getDataValue(dataSource, item.name));
      });
    }
    const { params, image, resources } = resourceParam.data;

    const parentObjectData = getDataValue(dataSource, 'params');
    const paramsContent = map(params, (value: any, itemKey: string) => {
      const inputKey = parentKey ? `${parentKey}.params.${itemKey}` : `params.${itemKey}`;
      return renderObject(value, inputKey, getDataValue(parentObjectData, itemKey));
    });

    return (
      <>
        {actionConfig.name === 'custom-script' ? (
          <div>{renderObject(image, 'resource.image', getDataValue(dataSource, 'image'))}</div>
        ) : null}
        <div>
          <div className="resource-input-group-title">{i18nMap.params}: </div>
          {paramsContent}
        </div>
        <div>{renderObject(resources, 'resource.resources', getDataValue(dataSource, 'resources'))}</div>
      </>
    );
  };

  const renderObject = (value: any, parentKey: string, dataSource?: any) => {
    if (!isObject(value.type)) {
      return renderPropertyValue(value, parentKey, dataSource);
    }

    if (value.type === 'string_array') {
      return renderStringArray(value, parentKey);
    }

    if (value.type === 'struct_array') {
      return renderStructArray(value, parentKey);
    }

    if (value.type === 'map') {
      return renderMap(value, parentKey, dataSource);
    }

    const content = renderResource({ data: value.struct }, parentKey, dataSource);
    if (!content || !Object.values(content).some((c) => c)) return null;

    return (
      <div key={parentKey}>
        <span className="resource-input-group-title">{i18nMap[value.name] || value.name}: </span>
        <div>{content}</div>
      </div>
    );
  };

  const renderMap = (value: any, parentKey: string, dataSource?: any) => {
    let initialValue = isCreate ? value.default : value.value || value.default;

    if (dataSource) {
      initialValue = dataSource;
    }

    if (!editing && !initialValue) {
      return null;
    }

    const inputField = (
      <Item
        key={parentKey}
        name={formKeyFormat(parentKey)}
        initialValue={initialValue}
        rules={[
          {
            required: value.required,
            message: i18n.t('dop:this item cannot be empty'),
          },
        ]}
      >
        <VariableInput disabled={!editing} label={getLabel(value.name, value.desc)} />
      </Item>
    );
    return inputField;
  };

  const renderStringArray = (value: any, parentKey: string) => {
    const inputField = (
      <Item
        key={parentKey}
        name={formKeyFormat(parentKey)}
        initialValue={isCreate ? value.default : value.value || value.default}
        rules={[
          {
            required: value.required,
            message: i18n.t('dop:this item cannot be empty'),
          },
        ]}
        getValueFromEvent={(val: Array<{ value: string }>) => {
          return val?.length ? val.map((v) => v.value) : val;
        }}
      >
        <ListInput disabled={!editing} label={getLabel(value.name, value.desc)} />
      </Item>
    );
    return inputField;
  };

  const renderPropertyValue = (value: any, parentKey: string, dataSource?: any) => {
    let input;
    let initialValue = isCreate ? value.default : value.value || value.default;

    if (dataSource) {
      initialValue = dataSource;
    }

    if (!editing && !initialValue) {
      return null;
    }

    const unit = value.unit ? <span>{value.unit}</span> : null;

    switch (value.type) {
      case 'float':
      case 'int':
        input = (
          <InputNumber
            disabled={!editing || value.readOnly}
            className="w-full"
            placeholder={i18n.t('dop:please enter data')}
          />
        );
        break;
      default:
        input = (
          <Input
            disabled={!editing || value.readOnly}
            placeholder={i18n.t('dop:please enter data')}
            addonAfter={unit}
          />
        );
        break;
    }

    const inputField = (
      <Item
        key={parentKey}
        label={getLabel(value.name, value.desc)}
        name={formKeyFormat(parentKey)}
        initialValue={initialValue}
        rules={[
          {
            required: value.required,
            message: i18n.t('dop:this item cannot be empty'),
          },
        ]}
      >
        {input}
      </Item>
    );
    return inputField;
  };

  const getLabel = (label: string, labelTip: string) => {
    let _label: any = label;
    if (labelTip) {
      _label = (
        <span>
          {_label}&nbsp;
          <Tooltip title={labelTip}>
            <ErdaIcon type="help" size="14" className="mr-1 align-middle text-icon" />
          </Tooltip>
        </span>
      );
    }
    return _label;
  };

  const renderStructArray = (property: any, parentKey: string) => {
    if ((!editing && !property.value) || (!editing && property.value && !property.value.length)) {
      return null;
    }

    const addBtn = editing ? (
      <ErdaIcon
        type="plus"
        className="cursor-pointer align-middle"
        onClick={() => addNewItemToStructArray(property, property.struct[0])}
      />
    ) : null;
    // getFieldDecorator(`${parentKey}-data`, { initialValue: property.value || [] });
    const data = property.value || []; // getFieldValue(`${parentKey}-data`);
    const _val = form.getFieldsValue();
    const realData = get(_val, `${parentKey}`) || [];
    const content = data.map((item: any, index: number) => {
      const keys = Object.keys(item);
      const curItem = realData[index] || item;
      const nameKey = get(property.struct, '[0].name');
      const headName = curItem[nameKey] || (typeof curItem[keys[0]] === 'string' ? curItem[keys[0]] : 'module');
      if (typeof headName === 'object') {
        return (
          <div className="p-2 text-black-4">
            {i18n.t(
              'dop:Rendering multi-layer nested structures is not supported at this time, please go to text mode.',
            )}
          </div>
        );
      }

      const header = (
        <div className="flex items-center justify-between">
          <span className="truncate" title={headName}>
            {headName}
          </span>
          {editing ? (
            <CustomIcon
              onClick={() => deleteItemFromStructArray(property, index, parentKey)}
              className="icon-delete"
              type="sc1"
            />
          ) : null}
        </div>
      );
      return (
        <Panel key={`${parentKey}.${item.key}-${String(index)}`} header={header} forceRender>
          {renderResource({ data: property.struct }, `${parentKey}.[${index}]`, item)}
        </Panel>
      );
    });

    return (
      <div key={`${parentKey}`}>
        <span className="resource-input-group-title">
          {property.name}:{addBtn}
        </span>
        {data.length ? (
          <Collapse className="collapse-field my-2" accordion>
            {content}
          </Collapse>
        ) : null}
      </div>
    );
  };

  const deleteItemFromStructArray = (property: any, index: number, parentKey: string) => {
    if (!property.value) {
      // eslint-disable-next-line no-param-reassign
      property.value = [];
    }
    property.value.splice(index, 1);
    updater.resource(cloneDeep(resource));

    const formDatas = form.getFieldValue(`${parentKey}`.split('.'));
    formDatas?.splice(index, 1);
    const curFormData = form.getFieldsValue();
    set(curFormData, parentKey, formDatas);
    form.setFieldsValue(curFormData);
  };

  const addNewItemToStructArray = (property: any, struct: any) => {
    if (!property.value) {
      // eslint-disable-next-line no-param-reassign
      property.value = [];
    }
    property.value.push({
      [struct.name]: `module-${property.value.length + 1}`,
    });
    updater.resource(cloneDeep(resource));
  };

  const isObject = (inputType: string) => {
    return ['map', 'string_array', 'struct_array', 'struct'].includes(inputType);
  };

  const onSubmit = () => {
    form
      .validateFields()
      .then((values: any) => {
        let data = cloneDeep(values);
        const resources = head(filter(resource.data, (item) => item.name === 'resources'));
        const originResource = transform(
          get(resources, 'struct'),
          (result, item: { name: string; default: string | number }) => {
            const { name, default: d } = item;
            // eslint-disable-next-line no-param-reassign
            result[name] = +d;
          },
          {},
        );
        const editedResources = get(data, 'resource.resources') || {};
        forEach(Object.entries(editedResources), ([key, value]) => {
          editedResources[key] = +(value as string);
        });
        const isResourceDefault = isEqual(editedResources, originResource);

        if (isResourceDefault) {
          data = omit(data, ['resource.resources']);
        }

        const filledFieldsData = clearEmptyField(data);
        const resData = { ...filledFieldsData, action: chosenAction } as any;
        if (data.executionCondition) resData.executionCondition = data.executionCondition;
        handleSubmit(resData);
      })
      .catch(({ errorFields }: { errorFields: Array<{ name: any[]; errors: any[] }> }) => {
        form.scrollToField(errorFields[0].name);
      });
  };

  const clearEmptyField = (ObjData: any) => {
    const filledFields: string[] = [];
    const findData = (obj: any, parentArray: string[]) => {
      Object.keys(obj).forEach((key) => {
        const currentParent = [...parentArray, key];
        const value = get(obj, key);
        if (typeof value === 'object' && value !== null) {
          findData(value, currentParent);
        } else if (value || value === 0) {
          filledFields.push(currentParent.join('.'));
        }
      });
    };
    findData(ObjData, []);
    return pick(ObjData, filledFields);
  };

  const executionCondition = (
    <Item
      label={i18n.t('common:execution conditions')}
      name={'executionCondition'}
      initialValue={get(propsNodeData, 'if') || undefined}
      rules={[
        {
          required: false,
        },
      ]}
    >
      <Input disabled={!editing} placeholder={i18n.t('common:configure execution conditions')} />
    </Item>
  );

  const onValuesChange = () => {
    // use changeKey to tigger a rerender,
    updater.changeKey((prev: number) => prev + 1);
  };

  return (
    <Form form={form} onValuesChange={onValuesChange} layout="vertical" className="edit-service-container">
      {alert}
      {taskType}
      {loopData}
      {type ? taskName : null}
      {actionVersion}
      {executionCondition}

      {renderTaskTypeStructure()}
      {editing ? (
        <Button type="primary" ghost onClick={onSubmit}>
          {i18n.t('Save')}
        </Button>
      ) : null}
    </Form>
  );
}