lodash#range TypeScript Examples

The following examples show how to use lodash#range. 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: usePollPredictions.ts    From vvs-ui with GNU General Public License v3.0 6 votes vote down vote up
usePollPredictions = () => {
  const timer = useRef<NodeJS.Timeout>(null)
  const dispatch = useAppDispatch()
  const { account } = useWeb3React()
  const currentEpoch = useGetCurrentEpoch()
  const earliestEpoch = useGetEarliestEpoch()
  const status = useGetPredictionsStatus()

  useEffect(() => {
    // Clear old timer
    if (timer.current) {
      clearInterval(timer.current)
    }

    if (status !== PredictionStatus.INITIAL) {
      timer.current = setInterval(async () => {
        const liveCurrentAndRecent = [currentEpoch, currentEpoch - 1, currentEpoch - 2]

        dispatch(fetchRounds(liveCurrentAndRecent))
        dispatch(fetchMarketData())

        if (account) {
          const epochRange = range(earliestEpoch, currentEpoch + 1)
          dispatch(fetchLedgerData({ account, epochs: epochRange }))
          dispatch(fetchClaimableStatuses({ account, epochs: epochRange }))
        }
      }, POLL_TIME_IN_SECONDS * 1000)
    }

    return () => {
      if (timer.current) {
        clearInterval(timer.current)
      }
    }
  }, [timer, account, status, currentEpoch, earliestEpoch, dispatch])
}
Example #2
Source File: list-container.editor.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
export function ListContainerEditor({
  nodeUid,
}: EditorComponentProps): React.ReactElement {
  const node = useBuilderNode<ListContainerProperties>({ nodeUid });
  /**
   * 提示:使用构件的属性配置来呈现该构件的关键 UI 特征。
   * 例如:对于按钮构件,根据 `buttonType` 来显示对应的背景色。
   */
  // const { someProps } = node.$$parsedProperties;
  return (
    <EditorContainer nodeUid={nodeUid}>
      <div className={styles.listContainer}>
        {range(0, 3).map((_, index) => (
          <div key={index} className={styles.content}></div>
        ))}
      </div>
    </EditorContainer>
  );
}
Example #3
Source File: LogCompression.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 6 votes vote down vote up
// 0.05%, or ~e^0.00005

describe('LogCompression', function () {
  let mock: Contract;

  before(async function () {
    mock = await deploy('MockLogCompression');
  });

  function valuesInMagnitude(power: number) {
    return [0.4, 1.2, 2.9, 3.3, 4.8, 5.3, 6.1, 7.4, 8.5, 9.4].map((x) => bn(x * 10 ** power));
  }

  function itRecoversOriginalValueWithError(minPower: number, maxPower: number, maxRelativeError: number) {
    for (const power of range(minPower, maxPower)) {
      it(`encodes and decodes powers of ${power}`, async () => {
        for (const original of valuesInMagnitude(power)) {
          const actual = await mock.fromLowResLog(await mock.toLowResLog(original));
          expectEqualWithError(actual, original, maxRelativeError);
        }
      });
    }
  }

  describe('small values', () => {
    itRecoversOriginalValueWithError(1, 5, 0.1); // Smaller values have larger error due to a lack of resolution
  });

  describe('medium and large values', () => {
    itRecoversOriginalValueWithError(5, 35, MAX_RELATIVE_ERROR);
  });
});
Example #4
Source File: general-radio.editor.tsx    From next-basics with GNU General Public License v3.0 6 votes vote down vote up
export function GeneralRadioEditor({
  nodeUid,
}: EditorComponentProps): React.ReactElement {
  const node = useBuilderNode<GeneralRadioProperties>({ nodeUid });

  const { label, type = "default", required } = 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.formRadioItem}>
          {range(0, 3).map((optionIndex) => (
            <span
              className={classNames(
                { [styles.option]: type === "default" },
                { [styles.buttonOption]: type === "button" }
              )}
              key={optionIndex}
            />
          ))}
        </div>
      </div>
    </EditorContainer>
  );
}
Example #5
Source File: usePollPredictions.ts    From glide-frontend with GNU General Public License v3.0 6 votes vote down vote up
usePollPredictions = () => {
  const timer = useRef<NodeJS.Timeout>(null)
  const dispatch = useAppDispatch()
  const { account } = useWeb3React()
  const currentEpoch = useGetCurrentEpoch()
  const earliestEpoch = useGetEarliestEpoch()
  const status = useGetPredictionsStatus()

  useEffect(() => {
    // Clear old timer
    if (timer.current) {
      clearInterval(timer.current)
    }

    if (status === PredictionStatus.LIVE) {
      timer.current = setInterval(async () => {
        const liveAndCurrent = [currentEpoch, currentEpoch - 1]

        dispatch(fetchRounds(liveAndCurrent))
        dispatch(fetchMarketData())

        if (account) {
          const epochRange = range(earliestEpoch, currentEpoch + 1)
          dispatch(fetchLedgerData({ account, epochs: epochRange }))
          dispatch(fetchClaimableStatuses({ account, epochs: epochRange }))
        }
      }, POLL_TIME_IN_SECONDS * 1000)
    }

    return () => {
      if (timer.current) {
        clearInterval(timer.current)
      }
    }
  }, [timer, account, status, currentEpoch, earliestEpoch, dispatch])
}
Example #6
Source File: GeneMapAxis.tsx    From nextclade with MIT License 6 votes vote down vote up
export function GeneMapAxis() {
  const genomeSize = useRecoilValue(genomeSizeAtom)
  const geneMap = useRecoilValue(geneMapAtom)
  const viewedGene = useRecoilValue(viewedGeneAtom)

  const { ticks, domain } = useMemo(() => {
    const length = getAxisLength(genomeSize, viewedGene, geneMap)
    const tickSize = getTickSize(length)
    const domain: [number, number] = [0, length]
    const ticks = range(0, length, tickSize)
    return { ticks, domain }
  }, [geneMap, genomeSize, viewedGene])

  return (
    <ResponsiveContainer width="100%" height={30}>
      <ComposedChart margin={MARGIN}>
        <XAxis dataKey={'ticks'} type="number" ticks={ticks} domain={domain} axisLine={false} />
      </ComposedChart>
    </ResponsiveContainer>
  )
}
Example #7
Source File: chain-limits.test.ts    From airnode with MIT License 6 votes vote down vote up
createRequests = (apiCallsCount: number, withdrawalCount: number): GroupedRequests => {
  // We want the generated requests to be sorted by the order of execution
  let blockNumber = 12345;

  return {
    apiCalls: range(apiCallsCount).map(() => {
      return fixtures.requests.buildApiCall({
        metadata: fixtures.requests.buildMetadata({ blockNumber: blockNumber++ }),
      });
    }),
    withdrawals: range(withdrawalCount).map(() => {
      return fixtures.requests.buildApiCall({
        metadata: fixtures.requests.buildMetadata({ blockNumber: blockNumber++ }),
      });
    }),
  };
}
Example #8
Source File: range-selection.ts    From S2 with MIT License 6 votes vote down vote up
private handleSeriesNumberRowSelected(
    startIndex: number,
    endIndex: number,
    cell: S2CellType<ViewMeta>,
  ) {
    // table模式下序列号行头
    const cellIdSufFix = this.spreadsheet.facet.layoutResult.colLeafNodes[0].id;
    return range(startIndex, endIndex + 1).map((row) => {
      const cellIdPrefix = String(row);
      return {
        id: cellIdPrefix + '-' + cellIdSufFix,
        colIndex: 0,
        rowIndex: row,
        type: cell.cellType,
      };
    });
  }
Example #9
Source File: testUtils.ts    From excalideck with MIT License 5 votes vote down vote up
export function makeRandomSlides(length = 10): Slide[] {
    return range(0, length).map(makeRandomSlide);
}
Example #10
Source File: useBuilderNodeMountPoints.spec.tsx    From next-core with GNU General Public License v3.0 5 votes vote down vote up
// Given a tree:
// - 1:
//   - toolbar:
//     - 2
//   - content:
//     - 3
//       - content
//         - 5
//           - <template-internal>
//             - 6
//           - items
//             - 7
//     - 4
(useBuilderData as jest.MockedFunction<typeof useBuilderData>).mockReturnValue({
  rootId: 1,
  nodes: range(1, 8).map(
    ($$uid) =>
      ({
        $$uid,
      } as Partial<BuilderRuntimeNode> as BuilderRuntimeNode)
  ),
  edges: [
    {
      parent: 1,
      child: 2,
      mountPoint: "toolbar",
      sort: 0,
    },
    {
      parent: 1,
      child: 3,
      mountPoint: "content",
      sort: 1,
    },
    {
      parent: 1,
      child: 4,
      mountPoint: "content",
      sort: 2,
    },
    {
      parent: 3,
      child: 5,
      mountPoint: "content",
      sort: 0,
    },
    {
      parent: 5,
      child: 6,
      mountPoint: "<internal>",
      sort: 0,
      $$isTemplateInternal: true,
    },
    {
      parent: 5,
      child: 7,
      mountPoint: "items",
      sort: 1,
      $$isTemplateDelegated: true,
    },
  ],
});
Example #11
Source File: generate-auto-extractors.spec.ts    From js-client with MIT License 5 votes vote down vote up
describe('generateAutoExtractors()', () => {
	const generateAutoExtractors = makeGenerateAutoExtractors(TEST_BASE_API_CONTEXT);

	// Use a randomly generated tag, so that we know exactly what we're going to query
	const tag = uuidv4();

	// The number of entries to generate
	const count = 1000;

	// The start date for generated queries
	const start = new Date(2010, 0, 0);

	// The end date for generated queries; one minute between each entry
	const end = addMinutes(start, count - 1);

	beforeAll(async () => {
		// Generate and ingest some entries
		const ingestJSONEntries = makeIngestJSONEntries(TEST_BASE_API_CONTEXT);
		const values: Array<CreatableJSONEntry> = range(0, count).map(i => {
			const timestamp = addMinutes(start, i).toISOString();
			return {
				timestamp,
				tag,
				data: JSON.stringify({ timestamp, value: i }, null, 2), // Add vertical whitespace, so that JSON is recommended over CSV
			};
		});

		await ingestJSONEntries(values);

		// Check the list of tags until our new tag appears
		const getAllTags = makeGetAllTags(TEST_BASE_API_CONTEXT);
		while (!(await getAllTags()).includes(tag)) {
			// Give the backend a moment to catch up
			await sleep(1000);
		}
	}, 25000);

	it(
		'Should generate auto extractors',
		integrationTest(async () => {
			const subscribeToOneSearch = makeSubscribeToOneSearch(TEST_BASE_API_CONTEXT);
			const query = `tag=${tag} limit 10`;
			const search = await subscribeToOneSearch(query, { filter: { dateRange: { start, end } } });

			const entries = await lastValueFrom(
				search.entries$.pipe(
					map(e => e as RawSearchEntries),
					takeWhile(e => !e.finished, true),
				),
			);

			const exploreResults = await generateAutoExtractors({ tag, entries });

			expect(Object.keys(exploreResults).length).withContext('we should get >0 AX suggestions').toBeGreaterThan(0);
			expect(exploreResults['json']).withContext('we should have a JSON AX suggestion').toBeDefined();
			expect(exploreResults['json'].length).withContext('we should have >0 JSON AX suggestions').toBeGreaterThan(0);

			exploreResults['json'].forEach(ax => {
				expect(ax.autoExtractor.tag).withContext('the suggested AX tag should match the provided tag').toEqual(tag);
				expect(ax.autoExtractor.module).withContext('the suggested AX module should be json').toEqual('json');
				expect(ax.confidence).withContext('json is the right module, so its confidence should be 10').toEqual(10);
				expect(ax.autoExtractor.parameters)
					.withContext('the suggested AX module should break out the fields in the entries')
					.toEqual('timestamp value');
				expect(ax.explorerEntries.length).withContext('explore should have >0 elements').toBeGreaterThan(0);
				ax.explorerEntries.forEach(exploreEntry => {
					expect(exploreEntry.elements.length)
						.withContext('explore should have two elements: one for each field (timestamp and value)')
						.toEqual(2);
					exploreEntry.elements.forEach(elt => {
						expect(elt.filters.length).withContext('we should have filters on explore elements').toBeGreaterThan(0);
					});
				});
			});

			// We can't really make assertions about what the other AX generators are going to do when we look at JSON data
		}),
		25000,
	);
});
Example #12
Source File: general-card.editor.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
export function GeneralCardEditor({
  nodeUid,
  editorProps = {},
}: EditorProperties): React.ReactElement {
  const node = useBuilderNode<GeneralCardProperties>({ nodeUid });
  const { editorStyle, hasHeader, slots, hasIcon } = editorProps;
  const hasSlots = useMemo(() => !isEmpty(slots), [slots]);

  const noSlotContent = useMemo(
    () => (
      <div className={styles.noSlots}>
        {hasIcon ? (
          <>
            <div>
              {range(0, 2).map((_, index) => (
                <div key={index} className={styles.text}></div>
              ))}
            </div>
            <div className={styles.icon}></div>
          </>
        ) : (
          <div className={styles.content}></div>
        )}
      </div>
    ),
    [hasIcon]
  );

  return (
    <EditorContainer nodeUid={nodeUid}>
      <div className={styles.cardContainer} style={editorStyle}>
        {hasHeader && <div className={styles.header}></div>}
        <div className={styles.body}>
          {hasSlots
            ? slots.map((slot, index) => (
                <SlotContainer
                  key={index}
                  nodeUid={nodeUid}
                  slotName={slot}
                  slotContainerStyle={{ marginBottom: 20 }}
                ></SlotContainer>
              ))
            : noSlotContent}
        </div>
      </div>
    </EditorContainer>
  );
}
Example #13
Source File: DebugEvents.tsx    From jitsu with MIT License 5 votes vote down vote up
DebugEvents = ({ handleClick }: Props) => {
  const services = ApplicationServices.get()

  const { data: eventsData, isLoading } = useLoaderAsObject(
    async () =>
      await services.backendApiClient.get(`/events/cache?project_id=${services.activeProject.id}&limit=10`, {
        proxy: true,
      })
  )

  const allEvents = useMemo(() => {
    const events = eventsData?.events ?? []
    if (events.length > 100) events.length = 100
    return events
      .map(event => ({
        data: event,
        time: moment(event.original._timestamp),
      }))
      .sort((e1: Event, e2: Event) => {
        if (e1.time.isAfter(e2.time)) {
          return -1
        } else if (e2.time.isAfter(e1.time)) {
          return 1
        }
        return 0
      })
  }, [eventsData?.events])

  return (
    <Card bordered={false} className={`${styles.events}`}>
      {isLoading ? (
        <List
          dataSource={range(0, 25)}
          renderItem={() => (
            <Skeleton active title={false} paragraph={{ rows: 2, width: ["100%", "70%"] }} className="mb-2" />
          )}
        />
      ) : (
        <List
          className={`h-full w-full overflow-y-auto overflow-x-hidden ${styles.withSmallScrollbar}`}
          dataSource={allEvents}
          renderItem={(item: any) => {
            return (
              <div
                className={`flex flex-col items-stretch ${styles.eventItem}`}
                onClick={handleClick(item?.data.original)}
              >
                <p className="truncate mb-0">{item?.time?.utc?.()?.format?.()}</p>
                {item?.data?.original?.event_type ? (
                  <p className="truncate mb-0">{item?.data?.original?.event_type}</p>
                ) : (
                  ""
                )}
              </div>
            )
          }}
        />
      )}
    </Card>
  )
}
Example #14
Source File: max-by.spec.ts    From s-libs with MIT License 5 votes vote down vote up
describe('maxBy()', () => {
  it('can sort by any primitive', () => {
    expect(maxBy([0, 1, -1], identity)).toBe(1);
    expect(maxBy([false, true, false], identity)).toBe(true);
    expect(maxBy(['b', 'c', 'a'], identity)).toBe('c');
  });

  //
  // stolen from https://github.com/lodash/lodash
  //

  it('should provide correct iteratee arguments', () => {
    const spy = jasmine.createSpy();
    maxBy([1, 2, 3], spy);
    expect(spy.calls.first().args).toEqual([1]);
  });

  it('should treat sparse arrays as dense', () => {
    const array = [1];
    array[2] = 3;
    const spy = jasmine.createSpy().and.returnValue(true);

    maxBy(array, spy);

    expectCallsAndReset(spy, [1], [undefined], [3]);
  });

  it('should not iterate custom properties of arrays', () => {
    const array = [1];
    (array as any).a = 1;
    const spy = jasmine.createSpy();

    maxBy(array, spy);

    expectCallsAndReset(spy, [1]);
  });

  it('should ignore changes to `length`', () => {
    const array = [1];
    const spy = jasmine.createSpy().and.callFake(() => {
      array.push(2);
      return true;
    });

    maxBy(array, spy);

    expect(spy).toHaveBeenCalledTimes(1);
  });

  it('should work with extremely large arrays', () => {
    expect(maxBy(range(0, 5e5), identity)).toBe(499999);
  });

  it('should work with an `iteratee`', () => {
    expect(maxBy([1, 2, 3], (n) => -n)).toBe(1);
  });

  it('should work when `iteratee` returns +/-Infinity', () => {
    const value = -Infinity;
    const object = { a: value };

    const actual = maxBy([object, { a: value }], (obj: { a: number }) => obj.a);

    expect(actual).toBe(object);
  });
});
Example #15
Source File: brick-table.editor.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
export function BrickTableEditor({
  nodeUid,
}: EditorComponentProps): React.ReactElement {
  const node = useBuilderNode<BrickTableProps>({ nodeUid });
  const {
    columns: tableColumnsProps,
    configProps = {},
  } = node.$$parsedProperties;

  const rows = 3;
  const columns = tableColumnsProps?.length || 3;

  return (
    <EditorContainer nodeUid={nodeUid}>
      <div className={styles.table}>
        {range(0, rows).map((rowIndex) => (
          <div
            key={rowIndex}
            className={`${styles.row} ${
              rowIndex === 0 ? styles.head : styles.body
            }`}
          >
            {configProps.rowSelection && <span className={styles.checkbox} />}
            {range(0, columns).map((columnIndex) => {
              const curColumn = tableColumnsProps?.[columnIndex];
              const columnTitle = smartDisplayForEvaluableString(
                curColumn?.title
              );
              const isUseBrick = curColumn?.headerBrick;
              return (
                <div key={columnIndex} className={styles.cell}>
                  {rowIndex === 0 && columnTitle && !isUseBrick ? (
                    <span>{columnTitle}</span>
                  ) : (
                    <div className={styles.content}></div>
                  )}
                </div>
              );
            })}
          </div>
        ))}
      </div>
    </EditorContainer>
  );
}
Example #16
Source File: WeightedPool.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
describe('WeightedPool', function () {
  let allTokens: TokenList;

  const MAX_TOKENS = 20;

  const POOL_SWAP_FEE_PERCENTAGE = fp(0.01);
  const WEIGHTS = range(1000, 1000 + MAX_TOKENS); // These will be normalized to weights that are close to each other, but different

  sharedBeforeEach('deploy tokens', async () => {
    allTokens = await TokenList.create(MAX_TOKENS, { sorted: true, varyDecimals: true });
  });

  itPaysProtocolFeesFromInvariantGrowth(WeightedPoolType.WEIGHTED_POOL);

  describe('weights and scaling factors', () => {
    for (const numTokens of range(2, MAX_TOKENS + 1)) {
      context(`with ${numTokens} tokens`, () => {
        let pool: WeightedPool;
        let tokens: TokenList;

        sharedBeforeEach('deploy pool', async () => {
          tokens = allTokens.subset(numTokens);

          pool = await WeightedPool.create({
            poolType: WeightedPoolType.WEIGHTED_POOL,
            tokens,
            weights: WEIGHTS.slice(0, numTokens),
            swapFeePercentage: POOL_SWAP_FEE_PERCENTAGE,
          });
        });

        it('sets token weights', async () => {
          const normalizedWeights = await pool.getNormalizedWeights();

          expect(normalizedWeights).to.deep.equal(pool.normalizedWeights);
        });

        it('sets scaling factors', async () => {
          const poolScalingFactors = await pool.getScalingFactors();
          const tokenScalingFactors = tokens.map((token) => fp(10 ** (18 - token.decimals)));

          expect(poolScalingFactors).to.deep.equal(tokenScalingFactors);
        });
      });
    }
  });
});
Example #17
Source File: getFunctionStep.ts    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
function getStageNodesAndEdges(
  functionLinksEdges: GraphEdge[],
  firstStepId: string,
  rootId: string
): [
  stageNodes: CollectNode[],
  stageEdges: GraphEdge[],
  stepDescendantsMap: Map<string, Set<string>>
] {
  const stepLevelMap = new Map<string, number>();
  let maxLevel = 0;
  const stepDescendantsMap = new Map<string, Set<string>>();
  const walk = (
    id: string,
    level: number,
    descendantsCarry?: Set<Set<string>>
  ): void => {
    if (level > (stepLevelMap.get(id) ?? -1)) {
      // Take every step's max level.
      stepLevelMap.set(id, level);
    }
    if (level > maxLevel) {
      maxLevel = level;
    }
    let selfDesc = stepDescendantsMap.get(id);
    if (!selfDesc) {
      selfDesc = new Set();
      stepDescendantsMap.set(id, selfDesc);
    }
    const newDescendantsCarry = new Set(descendantsCarry);
    newDescendantsCarry.add(selfDesc);
    for (const edge of functionLinksEdges) {
      if (edge.source === id) {
        for (const desc of newDescendantsCarry) {
          desc.add(edge.target);
        }
        walk(edge.target, level + 1, newDescendantsCarry);
      }
    }
  };
  walk(firstStepId, 0, new Set());

  const stageNodes: CollectNode[] = range(0, maxLevel + 1).map(
    (level) =>
      ({
        type: "stage",
        id: `stage.${level}`,
      } as CollectNode)
  );

  const stageEdges: GraphEdge[] = stageNodes.map((node) => ({
    source: rootId,
    target: node.id,
    type: "layer",
  }));

  for (const [id, level] of stepLevelMap.entries()) {
    stageEdges.push({
      source: `stage.${level}`,
      target: id,
      type: "stage",
    });
  }

  return [stageNodes, stageEdges, stepDescendantsMap];
}
Example #18
Source File: range-selection.ts    From S2 with MIT License 5 votes vote down vote up
private bindDataCellClick() {
    this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event: Event) => {
      event.stopPropagation();
      const cell: DataCell = this.spreadsheet.getCell(event.target);
      const meta = cell.getMeta();
      const { interaction } = this.spreadsheet;

      if (!meta) {
        return;
      }

      const lastClickedCell = this.spreadsheet.store.get('lastClickedCell');
      const isShiftSelect =
        this.isRangeSelection && lastClickedCell?.cellType === cell.cellType;

      if (!isShiftSelect) {
        this.spreadsheet.store.set('lastClickedCell', cell);
        return;
      }

      const { start, end } = getRangeIndex(
        lastClickedCell.getMeta() as ViewMeta,
        cell.getMeta(),
      );

      const cells = range(start.colIndex, end.colIndex + 1).flatMap((col) => {
        const cellIdSuffix =
          this.spreadsheet.facet.layoutResult.colLeafNodes[col].id;
        return range(start.rowIndex, end.rowIndex + 1).map((row) => {
          const cellIdPrefix =
            this.spreadsheet.facet.getSeriesNumberWidth() ||
            this.spreadsheet.isTableMode()
              ? String(row)
              : this.spreadsheet.facet.layoutResult.rowLeafNodes[row].id;
          return {
            id: cellIdPrefix + '-' + cellIdSuffix,
            colIndex: col,
            rowIndex: row,
            type: cell.cellType,
          };
        });
      });

      interaction.addIntercepts([InterceptType.CLICK, InterceptType.HOVER]);
      interaction.changeState({
        cells,
        stateName: InteractionStateName.SELECTED,
      });
      this.spreadsheet.showTooltipWithInfo(
        event,
        getActiveCellsTooltipData(this.spreadsheet),
      );
      this.spreadsheet.emit(
        S2Event.GLOBAL_SELECTED,
        interaction.getActiveCells(),
      );
    });
  }
Example #19
Source File: LunrSearchEngineIndexer.test.ts    From backstage with Apache License 2.0 5 votes vote down vote up
describe('LunrSearchEngineIndexer', () => {
  let indexer: LunrSearchEngineIndexer;

  beforeEach(() => {
    jest.clearAllMocks();
    indexer = new LunrSearchEngineIndexer();
  });

  it('should index documents', async () => {
    const documents = [
      {
        title: 'testTerm',
        text: 'testText',
        location: 'test/location',
      },
    ];

    await TestPipeline.withSubject(indexer).withDocuments(documents).execute();

    expect(lunrBuilderAddSpy).toHaveBeenCalledWith(documents[0]);
  });

  it('should index documents in bulk', async () => {
    const documents = range(350).map(i => ({
      title: `Hello World ${i}`,
      text: 'Lorem Ipsum',
      location: `location-${i}`,
    }));

    await TestPipeline.withSubject(indexer).withDocuments(documents).execute();
    expect(lunrBuilderAddSpy).toHaveBeenCalledTimes(350);
  });

  it('should initialize schema', async () => {
    const documents = [
      {
        title: 'testTerm',
        text: 'testText',
        location: 'test/location',
        extra: 'field',
      },
    ];

    await TestPipeline.withSubject(indexer).withDocuments(documents).execute();

    // Builder ref should be set to location (and only once).
    expect(lunrBuilderRefSpy).toHaveBeenCalledTimes(1);
    expect(lunrBuilderRefSpy).toHaveBeenLastCalledWith('location');

    // Builder fields should be based on document fields.
    expect(lunrBuilderFieldSpy).toHaveBeenCalledTimes(4);
    expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('title');
    expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('text');
    expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('location');
    expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('extra');
  });

  it('should configure lunr pipeline', async () => {
    expect(lunrBuilderSearchPipelineAddSpy).toHaveBeenLastCalledWith(
      lunr.stemmer,
    );
    expect(lunrBuilderPipelineAddSpy).toHaveBeenCalledWith(
      ...[lunr.trimmer, lunr.stopWordFilter, lunr.stemmer],
    );
  });
});
Example #20
Source File: actions.test.ts    From airnode with MIT License 5 votes vote down vote up
describe('processRequests', () => {
  test.each(['legacy', 'eip1559'] as const)('processes requests for each EVM provider - txType: %s', async (txType) => {
    const { blockSpy, gasPriceSpy } = createAndMockGasTarget(txType);

    estimateGasWithdrawalMock.mockResolvedValueOnce(ethers.BigNumber.from(50_000));
    staticFulfillMock.mockResolvedValue({ callSuccess: true });
    fulfillMock.mockResolvedValue({
      hash: '0xad33fe94de7294c6ab461325828276185dff6fed92c54b15ac039c6160d2bac3',
    });

    const sponsorAddress = '0x641eeb15B15d8E2CFB5f9d6480B175d93c14e6B6';
    const apiCall = fixtures.requests.buildSuccessfulApiCall({
      id: '0x67caaa2862cf971502d5c5b3d94d09d15c770f3313e76aa95c296b6587e7e5f1',
      sponsorAddress,
    });
    const requests: GroupedRequests = { apiCalls: [apiCall], withdrawals: [] };

    const transactionCountsBySponsorAddress = { [sponsorAddress]: 5 };
    const allProviders = range(2)
      .map(() => fixtures.buildEVMProviderSponsorState({ requests, transactionCountsBySponsorAddress, sponsorAddress }))
      .map((initialState) => ({
        ...initialState,
        settings: {
          ...initialState.settings,
          chainOptions: { txType, fulfillmentGasLimit: 500_000 },
        },
      }));

    const workerOpts = fixtures.buildWorkerOptions();
    const [logs, res] = await providers.processRequests(allProviders, workerOpts);
    expect(logs).toEqual([]);

    expect(txType === 'legacy' ? blockSpy : gasPriceSpy).not.toHaveBeenCalled();
    expect(txType === 'eip1559' ? blockSpy : gasPriceSpy).toHaveBeenCalled();

    expect(res.evm.map((evm) => evm.requests.apiCalls[0])).toEqual(
      range(allProviders.length).map(() => ({
        ...apiCall,
        fulfillment: { hash: '0xad33fe94de7294c6ab461325828276185dff6fed92c54b15ac039c6160d2bac3' },
        nonce: 5,
      }))
    );
  });
});
Example #21
Source File: TimeGraph.tsx    From office-hours with GNU General Public License v3.0 4 votes vote down vote up
export default function TimeGraph({
  values,
  maxTime,
  firstHour,
  lastHour,
  width,
  height,
}: {
  values: number[];
  maxTime: number;
  firstHour: number;
  lastHour: number;
  width: number;
  height: number;
}): ReactElement {
  const {
    tooltipOpen,
    tooltipLeft,
    tooltipTop,
    tooltipData,
    hideTooltip,
    showTooltip,
  } = useTooltip<number>();

  const { containerRef, TooltipInPortal } = useTooltipInPortal();

  // bounds
  const xMax = width - RIGHT_MARGIN - LEFT_MARGIN;
  const yMax = height - TOP_MARGIN - BOTTOM_MARGIN;

  // scales, memoize for performance
  const xScale = useMemo(
    () =>
      scaleBand<number>({
        range: [0, xMax],
        round: true,
        domain: range(Math.max(0, firstHour), Math.min(lastHour + 1, 24) + 1),
        padding: BAR_PADDING,
      }),
    [xMax, firstHour, lastHour]
  );

  // number of minutes between each grid row line
  const gridRowInterval = maxTime >= 60 ? 60 : 30;
  const maxTickVal = Math.max(maxTime, gridRowInterval);

  const yScale = useMemo(
    () =>
      scaleLinear<number>({
        range: [yMax, 0],
        round: true,
        domain: [0, maxTickVal + 5],
      }),
    [yMax, maxTickVal]
  );
  const barWidth = xScale.bandwidth();
  // the tick values for the y axis
  const yAxisTickValues = range(
    gridRowInterval,
    maxTickVal + 1,
    gridRowInterval
  );

  return width < 10 ? null : (
    // relative position is needed for correct tooltip positioning
    <GraphContainer>
      <svg ref={containerRef} width={width} height={height}>
        <rect
          x={0}
          y={0}
          width={width}
          height={height}
          fill="rgba(0,0,0,0)"
          rx={14}
        />
        <GridRows
          top={TOP_MARGIN}
          left={LEFT_MARGIN}
          width={width - RIGHT_MARGIN - LEFT_MARGIN}
          scale={yScale}
          tickValues={yAxisTickValues}
          stroke="#cccccc"
        />
        <Group left={LEFT_MARGIN} top={TOP_MARGIN}>
          {values.map((value, i) => {
            const barHeight = yMax - yScale(value);
            const barX = xScale(i) + (barWidth * (1 + BAR_PADDING)) / 2;
            const barY = yMax - barHeight;
            const interactWithBar = () => {
              if (tooltipTimeout) clearTimeout(tooltipTimeout);
              const top = yMax - barHeight - TOP_MARGIN; // - VERTICAL_MARGIN - barHeight;
              const left = barX + barWidth;
              showTooltip({
                tooltipData: value,
                tooltipTop: top,
                tooltipLeft: left,
              });
            };
            return (
              <BarRounded
                key={`bar-${formatDateHour(i)}`}
                className="popularTimes__bar"
                x={barX}
                y={barY}
                width={barWidth}
                height={barHeight}
                radius={10}
                top
                fill="#40a9ff"
                onMouseLeave={() => {
                  tooltipTimeout = window.setTimeout(() => {
                    hideTooltip();
                  }, 300);
                }}
                onMouseOver={interactWithBar}
                onMouseDown={interactWithBar}
              />
            );
          })}
        </Group>
        <Group left={LEFT_MARGIN}>
          <AxisBottom
            top={yMax + TOP_MARGIN}
            scale={xScale}
            tickFormat={(hour: number) =>
              (hour - firstHour) % 3 == 0 ? formatDateHour(hour) : ""
            }
            tickLabelProps={() => ({
              fill: "",
              fontSize: 11,
              textAnchor: "middle",
            })}
          />
        </Group>
        <Group top={TOP_MARGIN} left={LEFT_MARGIN}>
          <AxisLeft
            scale={yScale}
            hideTicks={true}
            tickValues={yAxisTickValues}
            tickFormat={(hour: number) => formatWaitTime(hour)}
            tickLabelProps={() => ({
              fill: "",
              fontSize: 11,
              textAnchor: "end",
            })}
          />
        </Group>
      </svg>

      {tooltipOpen && tooltipData && (
        <TooltipInPortal
          key={Math.random()} // update tooltip bounds each render
          top={tooltipTop}
          left={tooltipLeft}
          style={tooltipStyles}
        >
          {formatWaitTime(tooltipData)}
        </TooltipInPortal>
      )}
    </GraphContainer>
  );
}
Example #22
Source File: ExperimentList.test.tsx    From kfp-tekton-backend with Apache License 2.0 4 votes vote down vote up
describe('ExperimentList', () => {
  let tree: ShallowWrapper | ReactWrapper;

  jest.spyOn(console, 'log').mockImplementation(() => null);

  const updateBannerSpy = jest.fn();
  const updateDialogSpy = jest.fn();
  const updateSnackbarSpy = jest.fn();
  const updateToolbarSpy = jest.fn();
  const historyPushSpy = jest.fn();
  const listExperimentsSpy = jest.spyOn(Apis.experimentServiceApi, 'listExperiment');
  const listRunsSpy = jest.spyOn(Apis.runServiceApi, 'listRuns');
  // We mock this because it uses toLocaleDateString, which causes mismatches between local and CI
  // test enviroments
  jest.spyOn(Utils, 'formatDateString').mockImplementation(() => '1/2/2019, 12:34:56 PM');

  function generateProps(): PageProps {
    return TestUtils.generatePageProps(
      ExperimentList,
      { pathname: RoutePage.EXPERIMENTS } as any,
      '' as any,
      historyPushSpy,
      updateBannerSpy,
      updateDialogSpy,
      updateToolbarSpy,
      updateSnackbarSpy,
    );
  }

  function mockListNExpperiments(n: number = 1) {
    return () =>
      Promise.resolve({
        experiments: range(n).map(i => ({
          id: 'test-experiment-id' + i,
          name: 'test experiment name' + i,
        })),
      });
  }

  async function mountWithNExperiments(
    n: number,
    nRuns: number,
    { namespace }: { namespace?: string } = {},
  ): Promise<void> {
    listExperimentsSpy.mockImplementation(mockListNExpperiments(n));
    listRunsSpy.mockImplementation(() => ({
      runs: range(nRuns).map(i => ({ id: 'test-run-id' + i, name: 'test run name' + i })),
    }));
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} namespace={namespace} />);
    await listExperimentsSpy;
    await listRunsSpy;
    await TestUtils.flushPromises();
    tree.update(); // Make sure the tree is updated before returning it
  }

  afterEach(() => {
    jest.resetAllMocks();
    jest.clearAllMocks();
    if (tree.exists()) {
      tree.unmount();
    }
  });

  it('renders an empty list with empty state message', () => {
    tree = shallow(<ExperimentList {...generateProps()} />);
    expect(tree).toMatchSnapshot();
  });

  it('renders a list of one experiment', async () => {
    tree = shallow(<ExperimentList {...generateProps()} />);
    tree.setState({
      displayExperiments: [
        {
          description: 'test experiment description',
          expandState: ExpandState.COLLAPSED,
          name: 'test experiment name',
        },
      ],
    });
    await listExperimentsSpy;
    await listRunsSpy;
    expect(tree).toMatchSnapshot();
  });

  it('renders a list of one experiment with no description', async () => {
    tree = shallow(<ExperimentList {...generateProps()} />);
    tree.setState({
      experiments: [
        {
          expandState: ExpandState.COLLAPSED,
          name: 'test experiment name',
        },
      ],
    });
    await listExperimentsSpy;
    await listRunsSpy;
    expect(tree).toMatchSnapshot();
  });

  it('renders a list of one experiment with error', async () => {
    tree = shallow(<ExperimentList {...generateProps()} />);
    tree.setState({
      experiments: [
        {
          description: 'test experiment description',
          error: 'oops! could not load experiment',
          expandState: ExpandState.COLLAPSED,
          name: 'test experiment name',
        },
      ],
    });
    await listExperimentsSpy;
    await listRunsSpy;
    expect(tree).toMatchSnapshot();
  });

  it('calls Apis to list experiments, sorted by creation time in descending order', async () => {
    await mountWithNExperiments(1, 1);
    expect(listExperimentsSpy).toHaveBeenLastCalledWith(...LIST_EXPERIMENT_DEFAULTS);
    expect(listRunsSpy).toHaveBeenLastCalledWith(
      undefined,
      5,
      'created_at desc',
      'EXPERIMENT',
      'test-experiment-id0',
      encodeURIComponent(
        JSON.stringify({
          predicates: [
            {
              key: 'storage_state',
              op: PredicateOp.NOTEQUALS,
              string_value: RunStorageState.ARCHIVED.toString(),
            },
          ],
        } as ApiFilter),
      ),
    );
    expect(tree.state()).toHaveProperty('displayExperiments', [
      {
        expandState: ExpandState.COLLAPSED,
        id: 'test-experiment-id0',
        last5Runs: [{ id: 'test-run-id0', name: 'test run name0' }],
        name: 'test experiment name0',
      },
    ]);
  });

  it('calls Apis to list experiments with namespace when available', async () => {
    await mountWithNExperiments(1, 1, { namespace: 'test-ns' });
    expect(listExperimentsSpy).toHaveBeenLastCalledWith(
      ...LIST_EXPERIMENT_DEFAULTS_WITHOUT_RESOURCE_REFERENCE,
      'NAMESPACE',
      'test-ns',
    );
  });

  it('has a Refresh button, clicking it refreshes the experiment list', async () => {
    await mountWithNExperiments(1, 1);
    const instance = tree.instance() as ExperimentList;
    expect(listExperimentsSpy.mock.calls.length).toBe(1);
    const refreshBtn = instance.getInitialToolbarState().actions[ButtonKeys.REFRESH];
    expect(refreshBtn).toBeDefined();
    await refreshBtn!.action();
    expect(listExperimentsSpy.mock.calls.length).toBe(2);
    expect(listExperimentsSpy).toHaveBeenLastCalledWith(...LIST_EXPERIMENT_DEFAULTS);
    expect(updateBannerSpy).toHaveBeenLastCalledWith({});
  });

  it('shows error banner when listing experiments fails', async () => {
    TestUtils.makeErrorResponseOnce(listExperimentsSpy, 'bad stuff happened');
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} />);
    await listExperimentsSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'bad stuff happened',
        message:
          'Error: failed to retrieve list of experiments. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  it('shows error next to experiment when listing its last 5 runs fails', async () => {
    // tslint:disable-next-line:no-console
    console.error = jest.spyOn(console, 'error').mockImplementation();

    listExperimentsSpy.mockImplementationOnce(() => ({ experiments: [{ name: 'exp1' }] }));
    TestUtils.makeErrorResponseOnce(listRunsSpy, 'bad stuff happened');
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} />);
    await listExperimentsSpy;
    await TestUtils.flushPromises();
    expect(tree.state()).toHaveProperty('displayExperiments', [
      {
        error: 'Failed to load the last 5 runs of this experiment',
        expandState: 0,
        name: 'exp1',
      },
    ]);
  });

  it('shows error banner when listing experiments fails after refresh', async () => {
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} />);
    const instance = tree.instance() as ExperimentList;
    const refreshBtn = instance.getInitialToolbarState().actions[ButtonKeys.REFRESH];
    expect(refreshBtn).toBeDefined();
    TestUtils.makeErrorResponseOnce(listExperimentsSpy, 'bad stuff happened');
    await refreshBtn!.action();
    expect(listExperimentsSpy.mock.calls.length).toBe(2);
    expect(listExperimentsSpy).toHaveBeenLastCalledWith(...LIST_EXPERIMENT_DEFAULTS);
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'bad stuff happened',
        message:
          'Error: failed to retrieve list of experiments. Click Details for more information.',
        mode: 'error',
      }),
    );
  });

  it('hides error banner when listing experiments fails then succeeds', async () => {
    TestUtils.makeErrorResponseOnce(listExperimentsSpy, 'bad stuff happened');
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} />);
    const instance = tree.instance() as ExperimentList;
    await listExperimentsSpy;
    await TestUtils.flushPromises();
    expect(updateBannerSpy).toHaveBeenLastCalledWith(
      expect.objectContaining({
        additionalInfo: 'bad stuff happened',
        message:
          'Error: failed to retrieve list of experiments. Click Details for more information.',
        mode: 'error',
      }),
    );
    updateBannerSpy.mockReset();

    const refreshBtn = instance.getInitialToolbarState().actions[ButtonKeys.REFRESH];
    listExperimentsSpy.mockImplementationOnce(() => ({ experiments: [{ name: 'experiment1' }] }));
    listRunsSpy.mockImplementationOnce(() => ({ runs: [{ name: 'run1' }] }));
    await refreshBtn!.action();
    expect(listExperimentsSpy.mock.calls.length).toBe(2);
    expect(updateBannerSpy).toHaveBeenLastCalledWith({});
  });

  it('can expand an experiment to see its runs', async () => {
    await mountWithNExperiments(1, 1);
    tree
      .find('.tableRow button')
      .at(0)
      .simulate('click');
    expect(tree.state()).toHaveProperty('displayExperiments', [
      {
        expandState: ExpandState.EXPANDED,
        id: 'test-experiment-id0',
        last5Runs: [{ id: 'test-run-id0', name: 'test run name0' }],
        name: 'test experiment name0',
      },
    ]);
  });

  it('renders a list of runs for given experiment', async () => {
    tree = shallow(<ExperimentList {...generateProps()} />);
    tree.setState({
      displayExperiments: [{ id: 'experiment1', last5Runs: [{ id: 'run1id' }, { id: 'run2id' }] }],
    });
    const runListTree = (tree.instance() as any)._getExpandedExperimentComponent(0);
    expect(runListTree.props.experimentIdMask).toEqual('experiment1');
  });

  it('navigates to new experiment page when Create experiment button is clicked', async () => {
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} />);
    const createBtn = (tree.instance() as ExperimentList).getInitialToolbarState().actions[
      ButtonKeys.NEW_EXPERIMENT
    ];
    await createBtn!.action();
    expect(historyPushSpy).toHaveBeenLastCalledWith(RoutePage.NEW_EXPERIMENT);
  });

  it('always has new experiment button enabled', async () => {
    await mountWithNExperiments(1, 1);
    const calls = updateToolbarSpy.mock.calls[0];
    expect(calls[0].actions[ButtonKeys.NEW_EXPERIMENT]).not.toHaveProperty('disabled');
  });

  it('enables clone button when one run is selected', async () => {
    await mountWithNExperiments(1, 1);
    (tree.instance() as any)._selectionChanged(['run1']);
    expect(updateToolbarSpy).toHaveBeenCalledTimes(2);
    expect(updateToolbarSpy.mock.calls[0][0].actions[ButtonKeys.CLONE_RUN]).toHaveProperty(
      'disabled',
      true,
    );
    expect(updateToolbarSpy.mock.calls[1][0].actions[ButtonKeys.CLONE_RUN]).toHaveProperty(
      'disabled',
      false,
    );
  });

  it('disables clone button when more than one run is selected', async () => {
    await mountWithNExperiments(1, 1);
    (tree.instance() as any)._selectionChanged(['run1', 'run2']);
    expect(updateToolbarSpy).toHaveBeenCalledTimes(2);
    expect(updateToolbarSpy.mock.calls[0][0].actions[ButtonKeys.CLONE_RUN]).toHaveProperty(
      'disabled',
      true,
    );
    expect(updateToolbarSpy.mock.calls[1][0].actions[ButtonKeys.CLONE_RUN]).toHaveProperty(
      'disabled',
      true,
    );
  });

  it('enables compare runs button only when more than one is selected', async () => {
    await mountWithNExperiments(1, 1);
    (tree.instance() as any)._selectionChanged(['run1']);
    (tree.instance() as any)._selectionChanged(['run1', 'run2']);
    (tree.instance() as any)._selectionChanged(['run1', 'run2', 'run3']);
    expect(updateToolbarSpy).toHaveBeenCalledTimes(4);
    expect(updateToolbarSpy.mock.calls[0][0].actions[ButtonKeys.COMPARE]).toHaveProperty(
      'disabled',
      true,
    );
    expect(updateToolbarSpy.mock.calls[1][0].actions[ButtonKeys.COMPARE]).toHaveProperty(
      'disabled',
      false,
    );
    expect(updateToolbarSpy.mock.calls[2][0].actions[ButtonKeys.COMPARE]).toHaveProperty(
      'disabled',
      false,
    );
  });

  it('navigates to compare page with the selected run ids', async () => {
    await mountWithNExperiments(1, 1);
    (tree.instance() as any)._selectionChanged(['run1', 'run2', 'run3']);
    const compareBtn = (tree.instance() as ExperimentList).getInitialToolbarState().actions[
      ButtonKeys.COMPARE
    ];
    await compareBtn!.action();
    expect(historyPushSpy).toHaveBeenLastCalledWith(
      `${RoutePage.COMPARE}?${QUERY_PARAMS.runlist}=run1,run2,run3`,
    );
  });

  it('navigates to new run page with the selected run id for cloning', async () => {
    await mountWithNExperiments(1, 1);
    (tree.instance() as any)._selectionChanged(['run1']);
    const cloneBtn = (tree.instance() as ExperimentList).getInitialToolbarState().actions[
      ButtonKeys.CLONE_RUN
    ];
    await cloneBtn!.action();
    expect(historyPushSpy).toHaveBeenLastCalledWith(
      `${RoutePage.NEW_RUN}?${QUERY_PARAMS.cloneFromRun}=run1`,
    );
  });

  it('enables archive button when at least one run is selected', async () => {
    await mountWithNExperiments(1, 1);
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.ARCHIVE).disabled).toBeTruthy();
    (tree.instance() as any)._selectionChanged(['run1']);
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.ARCHIVE).disabled).toBeFalsy();
    (tree.instance() as any)._selectionChanged(['run1', 'run2']);
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.ARCHIVE).disabled).toBeFalsy();
    (tree.instance() as any)._selectionChanged([]);
    expect(TestUtils.getToolbarButton(updateToolbarSpy, ButtonKeys.ARCHIVE).disabled).toBeTruthy();
  });

  it('renders experiment names as links to their details pages', async () => {
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} />);
    expect(
      (tree.instance() as ExperimentList)._nameCustomRenderer({
        id: 'experiment-id',
        value: 'experiment name',
      }),
    ).toMatchSnapshot();
  });

  it('renders last 5 runs statuses', async () => {
    tree = TestUtils.mountWithRouter(<ExperimentList {...generateProps()} />);
    expect(
      (tree.instance() as ExperimentList)._last5RunsCustomRenderer({
        id: 'experiment-id',
        value: [
          { status: NodePhase.SUCCEEDED },
          { status: NodePhase.PENDING },
          { status: NodePhase.FAILED },
          { status: NodePhase.UNKNOWN },
          { status: NodePhase.SUCCEEDED },
        ],
      }),
    ).toMatchSnapshot();
  });

  describe('EnhancedExperimentList', () => {
    it('defaults to no namespace', () => {
      render(<EnhancedExperimentList {...generateProps()} />);
      expect(listExperimentsSpy).toHaveBeenLastCalledWith(...LIST_EXPERIMENT_DEFAULTS);
    });

    it('gets namespace from context', () => {
      render(
        <NamespaceContext.Provider value='test-ns'>
          <EnhancedExperimentList {...generateProps()} />
        </NamespaceContext.Provider>,
      );
      expect(listExperimentsSpy).toHaveBeenLastCalledWith(
        ...LIST_EXPERIMENT_DEFAULTS_WITHOUT_RESOURCE_REFERENCE,
        'NAMESPACE',
        'test-ns',
      );
    });

    it('auto refreshes list when namespace changes', () => {
      const { rerender } = render(
        <NamespaceContext.Provider value='test-ns-1'>
          <EnhancedExperimentList {...generateProps()} />
        </NamespaceContext.Provider>,
      );
      expect(listExperimentsSpy).toHaveBeenCalledTimes(1);
      expect(listExperimentsSpy).toHaveBeenLastCalledWith(
        ...LIST_EXPERIMENT_DEFAULTS_WITHOUT_RESOURCE_REFERENCE,
        'NAMESPACE',
        'test-ns-1',
      );
      rerender(
        <NamespaceContext.Provider value='test-ns-2'>
          <EnhancedExperimentList {...generateProps()} />
        </NamespaceContext.Provider>,
      );
      expect(listExperimentsSpy).toHaveBeenCalledTimes(2);
      expect(listExperimentsSpy).toHaveBeenLastCalledWith(
        ...LIST_EXPERIMENT_DEFAULTS_WITHOUT_RESOURCE_REFERENCE,
        'NAMESPACE',
        'test-ns-2',
      );
    });

    it("doesn't keep error message for request from previous namespace", async () => {
      listExperimentsSpy.mockImplementation(() => Promise.reject('namespace cannot be empty'));
      const { rerender } = render(
        <MemoryRouter>
          <NamespaceContext.Provider value={undefined}>
            <EnhancedExperimentList {...generateProps()} />
          </NamespaceContext.Provider>
        </MemoryRouter>,
      );

      listExperimentsSpy.mockImplementation(mockListNExpperiments());
      rerender(
        <MemoryRouter>
          <NamespaceContext.Provider value={'test-ns'}>
            <EnhancedExperimentList {...generateProps()} />
          </NamespaceContext.Provider>
        </MemoryRouter>,
      );
      await act(TestUtils.flushPromises);
      expect(updateBannerSpy).toHaveBeenLastCalledWith(
        {}, // Empty object means banner has no error message
      );
    });
  });
});
Example #23
Source File: DataDocChartComposer.tsx    From querybook with Apache License 2.0 4 votes vote down vote up
DataDocChartComposerComponent: React.FunctionComponent<
    IProps & FormikProps<IChartFormValues>
> = ({
    meta,
    dataDocId,
    cellAboveId,
    values,
    dirty,
    handleSubmit,
    setFieldValue,
    setFieldTouched,
    isEditable,
}) => {
    const [formTab, setFormTab] = React.useState<'data' | 'chart' | 'visuals'>(
        'data'
    );
    const [showTable, setShowTable] = React.useState(true);
    // const [showSeriesAggTypes, setShowSeriesAggTypes] = React.useState<boolean>(
    //     !values.aggType || !Object.keys(values.aggSeries).length
    // );
    const [tableTab, setTableTab] = React.useState<'original' | 'transformed'>(
        values.aggregate || values.switch ? 'transformed' : 'original'
    );

    const [displayExecutionId, setDisplayExecutionId] = React.useState(
        values.executionId
    );
    const [displayStatementId, setDisplayStatementId] = React.useState(
        undefined
    );

    const {
        statementResultData,
        queryExecutions,
        statementExecutions,
    } = useChartSource(
        values.cellId,
        displayExecutionId,
        displayStatementId,
        setFieldValue.bind(null, 'cellId'),
        setFieldValue.bind(null, 'executionId'),
        setDisplayStatementId,
        values.limit
    );

    const chartData = React.useMemo(
        () =>
            statementResultData
                ? transformData(
                      statementResultData,
                      values.aggregate,
                      values.switch,
                      values.formatAggCol,
                      values.formatSeriesCol,
                      values.formatValueCols,
                      values.aggSeries,
                      values.sortIndex,
                      values.sortAsc,
                      values.xIndex
                  )
                : null,
        [
            statementResultData,
            values.aggregate,
            values.switch,
            values.formatAggCol,
            values.formatSeriesCol,
            values.formatValueCols,
            values.aggSeries,
            values.sortIndex,
            values.sortAsc,
            values.xIndex,
        ]
    );

    // getting redux state
    const queryCellOptions = useSelector((state: IStoreState) =>
        queryCellSelector(state, dataDocId)
    );

    React.useEffect(() => {
        if (
            values.sourceType === 'cell_above' &&
            values.cellId !== cellAboveId
        ) {
            setFieldValue('cellId', cellAboveId);
        } else if (
            (values.sourceType === 'cell' ||
                values.sourceType === 'execution') &&
            values.cellId == null
        ) {
            setFieldValue('cellId', cellAboveId ?? queryCellOptions?.[0].id);
        }
    }, [values.sourceType]);

    React.useEffect(() => {
        if (chartData && values.aggType) {
            handleAggTypeChange(values.aggType);
        }
    }, [(chartData || [])[0]?.length]);

    React.useEffect(() => {
        setDisplayExecutionId(values.executionId);
    }, [values.executionId]);

    // ------------- select options ------------------------------------------------------------------
    const cellExecutionOptions = React.useMemo(() => {
        if (queryExecutions) {
            return queryExecutions.map((queryExecution) => ({
                value: queryExecution.id,
                label: `Execution ${queryExecution.id}`,
            }));
        } else if (displayExecutionId) {
            return [
                {
                    value: displayExecutionId,
                    label: `Execution ${displayExecutionId}`,
                },
            ];
        } else {
            return [
                {
                    value: -1,
                    label: `loading executions`,
                },
            ];
        }
    }, [displayExecutionId, queryExecutions]);

    const xAxisOptions = React.useMemo(() => {
        if (!chartData) {
            return [];
        }
        return chartData[0].map((col, idx) => ({
            value: idx,
            label: col,
        }));
    }, [chartData]);

    const formatAggByOptions = React.useMemo(() => {
        if (!statementResultData) {
            return [];
        }
        return statementResultData[0].reduce<Array<ISelectOption<number>>>(
            (optionsAcc, label, idx) => {
                if (idx !== values.formatSeriesCol) {
                    optionsAcc.push({
                        value: idx,
                        label,
                    });
                }
                return optionsAcc;
            },
            []
        );
    }, [statementResultData, values.formatSeriesCol]);

    const makeFormatSeriesOptions = React.useMemo(() => {
        if (!statementResultData) {
            return [];
        }
        const columns = statementResultData[0];
        const options: Array<ISelectOption<number>> = [];
        for (let i = 0; i < columns.length; i++) {
            if (i !== values.formatAggCol) {
                options.push({
                    value: i,
                    label: columns[i],
                });
            }
        }
        return options;
    }, [statementResultData, values.formatAggCol]);

    const makeFormatValueOptions = React.useMemo(() => {
        if (!statementResultData) {
            return [];
        }
        const columns = statementResultData[0];
        const options: Array<ISelectOption<number>> = [];
        for (let i = 0; i < columns.length; i++) {
            if (i !== values.xIndex && i !== values.formatSeriesCol) {
                options.push({
                    value: i,
                    label: columns[i],
                });
            }
        }
        return options;
    }, [statementResultData, values.formatSeriesCol, values.xIndex]);

    const makeSeriesValsAndOptions = React.useCallback(
        (selectedValues: boolean) => {
            if (!chartData || !chartData.length) {
                return [{ value: 0, label: 'loading series' }];
            }
            const valsArray = chartData[0];
            const optionIdxs = selectedValues
                ? range(valsArray.length).filter(
                      (val) =>
                          !values.hiddenSeries.includes(val) &&
                          val !== values.xIndex
                  )
                : values.hiddenSeries;
            const options = optionIdxs.map((i) => ({
                value: i,
                label: valsArray[i],
                color:
                    ColorPalette[
                        values.coloredSeries[i] ?? i % ColorPalette.length
                    ].color,
            }));
            return options;
        },
        [chartData, values.xIndex, values.hiddenSeries, values.coloredSeries]
    );

    const getAxesScaleType = React.useCallback(
        (colIndex: number) => {
            for (let row = 1; row < chartData?.length; row++) {
                const cell = chartData?.[row]?.[colIndex];
                if (cell != null) {
                    return getDefaultScaleType(chartData?.[row]?.[colIndex]);
                }
            }
            // Unknown type, use linear as a default
            return 'linear';
        },
        [chartData]
    );

    const seriesColorOptions = ColorPalette.map((color, idx) => ({
        value: idx,
        label: color.name,
        color: color.color,
    }));

    const seriesAggOptions = Object.entries(aggTypes).map(([val, key]) => ({
        value: val as ChartDataAggType,
        label: key,
    }));

    // ------------- event handlers ------------------------------------------------------------------
    const handleHiddenSeriesChange = (
        selectedVals: Array<ISelectOption<number>>
    ) => {
        const hiddenSeries = [];
        const selectedSeries = selectedVals.map((obj) => obj.value);
        for (let i = 0; i < chartData[0].length; i++) {
            if (i !== values.xIndex && !selectedSeries.includes(i)) {
                hiddenSeries.push(i);
            }
        }
        setFieldValue('hiddenSeries', hiddenSeries);
    };

    const handleAggTypeChange = (aggType: ChartDataAggType) => {
        setFieldValue('aggType', aggType);

        const newAggSeries = {};
        for (let i = 0; i < chartData[0].length; i++) {
            newAggSeries[i] = aggType;
        }
        setFieldValue('aggSeries', newAggSeries);
    };

    // ------------- DOM elements ------------------------------------------------------------------
    const tabsDOM = (
        <Tabs
            selectedTabKey={formTab}
            onSelect={(selectedTab: 'data' | 'chart' | 'visuals') =>
                setFormTab(selectedTab)
            }
            items={formTabs}
            wide
        />
    );

    const renderPickerDOM = () => {
        if (values.sourceType === 'custom') {
            return null; // Custom data is sourced from internal context
        }

        const showQueryExecution =
            values.sourceType !== 'execution' && queryExecutions.length;
        const showStatementExecution =
            displayExecutionId != null && statementExecutions.length;

        const queryExecutionPicker =
            showQueryExecution || showStatementExecution ? (
                <FormField
                    label={`${
                        showQueryExecution ? 'Query Execution' : 'Statement'
                    } (Display Only)`}
                    stacked
                    help="Not Saved. Defaults to latest."
                >
                    {showQueryExecution && (
                        <QueryExecutionPicker
                            queryExecutionId={displayExecutionId}
                            onSelection={setDisplayExecutionId}
                            queryExecutions={queryExecutions}
                            autoSelect
                            shortVersion
                        />
                    )}

                    {showStatementExecution && (
                        <StatementExecutionPicker
                            statementExecutionId={displayStatementId}
                            onSelection={setDisplayStatementId}
                            statementExecutions={statementExecutions}
                            total={statementExecutions.length}
                            autoSelect
                        />
                    )}
                </FormField>
            ) : null;

        return (
            <div className="DataDocChartComposer-exec-picker">
                {queryExecutionPicker}
            </div>
        );
    };

    const dataSourceDOM = (
        <>
            <FormSectionHeader>Source</FormSectionHeader>
            <FormField stacked label="Type">
                <SimpleReactSelect
                    value={values.sourceType}
                    onChange={(val) => {
                        if (
                            values.sourceType === 'execution' &&
                            val !== 'execution'
                        ) {
                            setFieldValue('executionId', null);
                        }
                        setFieldValue('sourceType', val);
                    }}
                    options={Object.entries(sourceTypes).map(([key, val]) => ({
                        value: key,
                        label: val,
                    }))}
                />
            </FormField>
            {values.sourceType !== 'cell_above' ? (
                <FormField stacked label="Cell">
                    <ReactSelectField
                        name="cellId"
                        options={queryCellOptions.map((val) => ({
                            value: val.id,
                            label: val.title,
                        }))}
                    />
                </FormField>
            ) : null}
            {values.sourceType === 'execution' ? (
                <SimpleField
                    stacked
                    type="react-select"
                    options={cellExecutionOptions}
                    name="executionId"
                    label="Execution"
                />
            ) : null}
            {renderPickerDOM()}
            <SimpleField
                stacked
                type="react-select"
                options={StatementExecutionResultSizes.map((size) => ({
                    value: size,
                    label: formatNumber(size, 'Row'),
                }))}
                name="limit"
                label="Row Limit"
                help="Max number of rows to fetch from the result"
            />
        </>
    );

    const dataTransformationDOM = (
        <>
            <FormSectionHeader>Transformation</FormSectionHeader>
            <SimpleField
                type="checkbox"
                name="aggregate"
                label="Aggregate"
                onChange={(val) => {
                    setFieldValue('aggregate', val);
                    setFieldValue('hiddenSeries', []);
                    if (val) {
                        handleAggTypeChange('sum');
                        setTableTab('transformed');
                    } else {
                        setFieldValue('aggType', undefined);
                        setFieldValue('aggSeries', {});
                    }
                }}
                help="By default, all rows will be aggregated"
            />
            {values.aggregate ? (
                <>
                    <FormField stacked label="Aggregate By">
                        <SimpleReactSelect
                            value={values.aggType}
                            onChange={(val) => handleAggTypeChange(val)}
                            options={seriesAggOptions}
                        />
                    </FormField>
                    <div className="DataDocChartComposer-info m8">
                        Value must be selected for aggregation by row/column.
                    </div>
                    <SimpleField
                        stacked
                        type="react-select"
                        label="Row"
                        name="formatAggCol"
                        options={formatAggByOptions}
                        isDisabled={!statementResultData}
                        withDeselect
                    />
                    <SimpleField
                        stacked
                        label="Column"
                        type="react-select"
                        name="formatSeriesCol"
                        options={makeFormatSeriesOptions}
                        isDisabled={!statementResultData}
                        withDeselect
                    />
                    <SimpleField
                        stacked
                        label="Value"
                        type="react-select"
                        name="formatValueCols"
                        value={values.formatValueCols[0]}
                        options={makeFormatValueOptions}
                        isDisabled={!statementResultData}
                        onChange={(val) => {
                            if (val == null) {
                                setFieldValue('formatValueCols', []);
                            } else {
                                setFieldValue('formatValueCols', [val]);
                            }
                        }}
                        withDeselect
                    />
                </>
            ) : null}
            <SimpleField
                type="checkbox"
                label="Switch Rows/Columns"
                name="switch"
                help="Switch is applied after aggregation"
            />
        </>
    );

    const dataTabDOM = (
        <>
            {dataSourceDOM}
            {dataTransformationDOM}
        </>
    );

    const chartOptionsDOM = (
        <>
            <FormField stacked label="Type">
                <SimpleReactSelect
                    value={values.chartType}
                    onChange={(val) => {
                        setFieldValue('chartType', val);
                        // area defaults to true
                        if (val === 'area') {
                            setFieldValue('stack', true);
                        } else if (
                            [
                                'line',
                                'pie',
                                'doughnut',
                                'scatter',
                                'bubble',
                            ].includes(val)
                        ) {
                            // these charts cannot be stacked
                            setFieldValue('stack', false);
                            if (val === 'bubble' && !values.zIndex) {
                                setFieldValue('zIndex', 2);
                            }
                        }
                    }}
                    options={Object.entries(chartTypes).map(([key, val]) => ({
                        value: key,
                        label: val,
                    }))}
                />
            </FormField>
            {['bar', 'histogram'].includes(values.chartType) ? (
                <SimpleField type="checkbox" label="Stack Chart" name="stack" />
            ) : null}
        </>
    );

    let axesDOM: React.ReactChild = null;

    if (values.chartType !== 'table') {
        const noAxesConfig = ['pie', 'doughnut'].includes(values.chartType);
        const getAxisDOM = (
            prefix: string,
            axisMeta: IChartAxisMeta,
            scaleType: ChartScaleType,
            scaleOptions: ChartScaleType[] = ChartScaleOptions
        ) => {
            if (noAxesConfig) {
                return null;
            }

            scaleType = getAutoDetectedScaleType(scaleOptions, scaleType);

            const allScaleOptions = [
                {
                    label: `auto detect (${scaleType})`,
                    value: null,
                },
            ].concat(
                scaleOptions.map((value) => ({
                    label: value,
                    value,
                }))
            );

            let axisRangeDOM: React.ReactNode;
            const assumedScale = axisMeta.scale ?? scaleType;
            if (assumedScale === 'linear' || assumedScale === 'logarithmic') {
                axisRangeDOM = (
                    <FormField stacked label="Range">
                        <Level margin="8px">
                            <NumberField
                                name={`${prefix}.min`}
                                placeholder="Min"
                            />
                            <NumberField
                                name={`${prefix}.max`}
                                placeholder="Max"
                            />
                        </Level>
                    </FormField>
                );
            }

            return (
                <>
                    <SimpleField
                        stacked
                        type="react-select"
                        name={`${prefix}.scale`}
                        options={allScaleOptions}
                    />
                    {axisRangeDOM}
                </>
            );
        };

        const detectedXAxisScale = getAxesScaleType(values.xIndex);
        const xAxisDOM = (
            <>
                <FormSectionHeader>X Axis</FormSectionHeader>
                <FormField stacked label="X Axis">
                    <ReactSelectField
                        name={`xIndex`}
                        options={xAxisOptions}
                        isDisabled={!statementResultData}
                    />
                </FormField>
                {getAxisDOM(
                    'xAxis',
                    values.xAxis,

                    detectedXAxisScale === 'linear'
                        ? 'category'
                        : detectedXAxisScale,
                    chartTypeToAllowedAxisType[values.chartType].x
                )}
            </>
        );

        let yAxisDOM: React.ReactChild;
        if (!noAxesConfig) {
            const yAxisSeries = makeSeriesValsAndOptions(true);
            const defaultYAxisScaleType = yAxisSeries.length
                ? getAxesScaleType(yAxisSeries[0].value)
                : null;
            yAxisDOM = (
                <>
                    <FormSectionHeader>Y Axis</FormSectionHeader>
                    <FormField stacked label="Series">
                        <Select
                            styles={defaultReactSelectStyles}
                            value={yAxisSeries}
                            onChange={(val: any[]) =>
                                handleHiddenSeriesChange(val)
                            }
                            options={makeSeriesValsAndOptions(false)}
                            isMulti
                        />
                    </FormField>
                    {getAxisDOM(
                        'yAxis',
                        values.yAxis,
                        defaultYAxisScaleType,
                        chartTypeToAllowedAxisType[values.chartType].y
                    )}
                </>
            );
        }

        const zAxisDOM =
            values.chartType === 'bubble' ? (
                <>
                    <FormSectionHeader>Z Axis</FormSectionHeader>
                    <FormField stacked label="Z Axis">
                        <ReactSelectField
                            name={`zIndex`}
                            options={xAxisOptions}
                            isDisabled={!statementResultData}
                        />
                    </FormField>
                </>
            ) : null;

        axesDOM = (
            <>
                {xAxisDOM}
                {yAxisDOM}
                {zAxisDOM}
            </>
        );
    }

    const sortDOM = (
        <>
            <FormSectionHeader>Sort</FormSectionHeader>
            <SimpleField
                stacked
                type="react-select"
                options={xAxisOptions}
                name="sortIndex"
                label="Sort Index"
                withDeselect
                onChange={(val) => {
                    setFieldValue('sortIndex', val);
                    if (val != null) {
                        setTableTab('transformed');
                    }
                }}
            />
            <SimpleField
                stacked
                type="react-select"
                options={[
                    { value: true, label: 'Ascending' },
                    { value: false, label: 'Descending' },
                ]}
                name="sortAsc"
                label="Sort Direction"
            />
        </>
    );

    const chartTabDOM = (
        <>
            {chartOptionsDOM}
            {axesDOM}
            {sortDOM}
        </>
    );

    const seriesColorDOM = chartData
        ? chartData[0].map((col, seriesIdx) => {
              if (seriesIdx === 0 || values.hiddenSeries.includes(seriesIdx)) {
                  return null;
              }
              const colorIdx =
                  seriesIdx in values.coloredSeries
                      ? values.coloredSeries[seriesIdx]
                      : seriesIdx % ColorPalette.length;
              return (
                  <FormField
                      stacked
                      key={col}
                      label={() => (
                          <>
                              <b>{col}</b> Color
                          </>
                      )}
                  >
                      <ReactSelectField
                          value={colorIdx}
                          name={`coloredSeries[${seriesIdx}]`}
                          options={seriesColorOptions}
                      />
                  </FormField>
              );
          })
        : null;

    const visualsTabDOM =
        values.chartType === 'table' ? (
            <FormField stacked label="Title">
                <Field name="title" />
            </FormField>
        ) : (
            <>
                <FormField stacked label="Title">
                    <Field name="title" />
                </FormField>
                {['pie', 'doughnut'].includes(values.chartType) ? null : (
                    <>
                        <SimpleField
                            stacked
                            label="X Axis Label"
                            name="xAxis.label"
                            type="input"
                        />
                        <SimpleField
                            stacked
                            label="Y Axis Label"
                            name="yAxis.label"
                            type="input"
                        />
                    </>
                )}
                <SimpleField
                    stacked
                    label="Chart Height"
                    name="size"
                    type="react-select"
                    help="If set from not auto to auto height, refresh the page to see change."
                    options={[
                        {
                            value: ChartSize.SMALL,
                            label: 'Small (1/3 height)',
                        },
                        {
                            value: ChartSize.MEDIUM,
                            label: 'Medium (1/2 height)',
                        },
                        {
                            value: ChartSize.LARGE,
                            label: 'Large (full height)',
                        },
                        {
                            value: ChartSize.AUTO,
                            label: 'Auto height',
                        },
                    ]}
                />
                <FormSectionHeader>Legend</FormSectionHeader>
                <SimpleField
                    label="Visible"
                    name="legendDisplay"
                    type="checkbox"
                />
                <SimpleField
                    stacked
                    label="Position"
                    name="legendPosition"
                    type="react-select"
                    options={['top', 'bottom', 'left', 'right']}
                />

                <FormSectionHeader>Values</FormSectionHeader>
                <SimpleField
                    stacked
                    label="Display"
                    name="valueDisplay"
                    type="react-select"
                    options={[
                        {
                            value: ChartValueDisplayType.FALSE,
                            label: 'Hide Values',
                        },
                        {
                            value: ChartValueDisplayType.TRUE,
                            label: 'Show Values',
                        },
                        {
                            value: ChartValueDisplayType.AUTO,
                            label: 'Show Values without Overlap',
                        },
                    ]}
                    onChange={(val) => {
                        setFieldValue('valueDisplay', val);
                        if (val) {
                            if (values.valuePosition == null) {
                                setFieldValue('valuePosition', 'center');
                            }
                            if (values.valueAlignment == null) {
                                setFieldValue('valueAlignment', 'center');
                            }
                        }
                    }}
                />
                {values.valueDisplay ? (
                    <>
                        <SimpleField
                            stacked
                            label="Position"
                            name="valuePosition"
                            type="react-select"
                            options={['center', 'start', 'end']}
                        />
                        <SimpleField
                            stacked
                            label="Alignment"
                            name="valueAlignment"
                            type="react-select"
                            options={[
                                'center',
                                'start',
                                'end',
                                'right',
                                'left',
                                'top',
                                'bottom',
                            ]}
                        />
                    </>
                ) : null}
                {['pie', 'doughnut', 'table'].includes(
                    values.chartType
                ) ? null : (
                    <>
                        <FormSectionHeader>Colors</FormSectionHeader>
                        {seriesColorDOM}
                    </>
                )}
                {['line', 'area'].includes(values.chartType) ? (
                    <>
                        <FormSectionHeader>Line Formatting</FormSectionHeader>
                        <SimpleField
                            label="Connect missing data"
                            name="connectMissing"
                            type="checkbox"
                        />
                    </>
                ) : null}
            </>
        );

    const formDOM = (
        <FormWrapper size={7} className="DataDocChartComposer-form">
            <DisabledSection disabled={!isEditable}>
                {formTab === 'data' && dataTabDOM}
                {formTab === 'chart' && chartTabDOM}
                {formTab === 'visuals' && visualsTabDOM}
            </DisabledSection>
        </FormWrapper>
    );

    const hideTableButtonDOM = (
        <IconButton
            icon={showTable ? 'ChevronDown' : 'ChevronUp'}
            onClick={() => setShowTable(!showTable)}
            noPadding
        />
    );

    let dataDOM: JSX.Element;
    let dataSwitch: JSX.Element;
    if (chartData && showTable) {
        if (values.aggregate || values.switch || values.sortIndex != null) {
            dataSwitch = (
                <div className="toggleTableDataSwitch">
                    <Tabs
                        selectedTabKey={tableTab}
                        onSelect={(key: 'original' | 'transformed') =>
                            setTableTab(key)
                        }
                        items={tableTabs}
                    />
                </div>
            );
        }

        dataDOM = (
            <div className="DataDocChartComposer-data">
                <StatementResultTable
                    data={
                        tableTab === 'original'
                            ? statementResultData
                            : chartData
                    }
                    paginate={true}
                    maxNumberOfRowsToShow={5}
                />
            </div>
        );
    }

    const tableDOM = (
        <div className="DataDocChartComposer-bottom">
            <Level>
                <LevelItem>{dataSwitch}</LevelItem>
                <LevelItem>{hideTableButtonDOM}</LevelItem>
            </Level>
            {dataDOM}
        </div>
    );

    const chartDOM =
        values.chartType === 'table' ? (
            <DataDocChartCellTable data={chartData} title={values.title} />
        ) : (
            <DataDocChart
                data={chartData}
                meta={formValsToMeta(values, meta)}
                chartJSOptions={{ maintainAspectRatio: false }}
            />
        );

    const makeLeftDOM = () => (
        <div className="DataDocChartComposer-left mr16">
            <div className="DataDocChartComposer-chart mt8">
                <div className="DataDocChartComposer-chart-sizer">
                    {chartData ? chartDOM : null}
                </div>
            </div>
            {tableDOM}
        </div>
    );

    return (
        <div className="DataDocChartComposer mh16 pb16">
            {makeLeftDOM()}
            <div className="DataDocChartComposer-right">
                {tabsDOM}
                {formDOM}
                {isEditable ? (
                    <div className="DataDocChartComposer-button">
                        <SoftButton
                            onClick={() => handleSubmit()}
                            title="Save"
                            fullWidth
                            pushable={false}
                        />
                    </div>
                ) : null}
            </div>
        </div>
    );
}
Example #24
Source File: ElasticSearchSearchEngineIndexer.test.ts    From backstage with Apache License 2.0 4 votes vote down vote up
describe('ElasticSearchSearchEngineIndexer', () => {
  let indexer: ElasticSearchSearchEngineIndexer;
  let bulkSpy: jest.Mock;
  let catSpy: jest.Mock;
  let createSpy: jest.Mock;
  let aliasesSpy: jest.Mock;
  let deleteSpy: jest.Mock;

  beforeEach(() => {
    // Instantiate the indexer to be tested.
    indexer = new ElasticSearchSearchEngineIndexer({
      type: 'some-type',
      indexPrefix: '',
      indexSeparator: '-index__',
      alias: 'some-type-index__search',
      logger: getVoidLogger(),
      elasticSearchClient: client,
    });

    // Set up all requisite Elastic mocks.
    mock.clearAll();
    bulkSpy = jest.fn().mockReturnValue({ took: 9, errors: false, items: [] });
    mock.add(
      {
        method: 'POST',
        path: '/_bulk',
      },
      bulkSpy,
    );
    mock.add(
      {
        method: 'GET',
        path: '/:index/_refresh',
      },
      jest.fn().mockReturnValue({}),
    );

    catSpy = jest.fn().mockReturnValue([
      {
        alias: 'some-type-index__search',
        index: 'some-type-index__123tobedeleted',
        filter: '-',
        'routing.index': '-',
        'routing.search': '-',
        is_write_index: '-',
      },
      {
        alias: 'some-type-index__search_removable',
        index: 'some-type-index__456tobedeleted',
        filter: '-',
        'routing.index': '-',
        'routing.search': '-',
        is_write_index: '-',
      },
    ]);
    mock.add(
      {
        method: 'GET',
        path: '/_cat/aliases/some-type-index__search%2Csome-type-index__search_removable',
      },
      catSpy,
    );

    createSpy = jest.fn().mockReturnValue({
      acknowledged: true,
      shards_acknowledged: true,
      index: 'single_index',
    });
    mock.add(
      {
        method: 'PUT',
        path: '/:index',
      },
      createSpy,
    );

    aliasesSpy = jest.fn().mockReturnValue({});
    mock.add(
      {
        method: 'POST',
        path: '*',
      },
      aliasesSpy,
    );

    deleteSpy = jest.fn().mockReturnValue({});
    mock.add(
      {
        method: 'DELETE',
        path: '/some-type-index__123tobedeleted%2Csome-type-index__456tobedeleted',
      },
      deleteSpy,
    );
  });

  it('indexes documents', async () => {
    const documents = [
      {
        title: 'testTerm',
        text: 'testText',
        location: 'test/location',
      },
      {
        title: 'Another test',
        text: 'Some more text',
        location: 'test/location/2',
      },
    ];

    await TestPipeline.withSubject(indexer).withDocuments(documents).execute();

    // Older indices should have been queried for.
    expect(catSpy).toHaveBeenCalled();

    // A new index should have been created.
    const createdIndex = createSpy.mock.calls[0][0].path.slice(1);
    expect(createdIndex).toContain('some-type-index__');

    // Bulk helper should have been called with documents.
    const bulkBody = bulkSpy.mock.calls[0][0].body;
    expect(bulkBody[0]).toStrictEqual({ index: { _index: createdIndex } });
    expect(bulkBody[1]).toStrictEqual(documents[0]);
    expect(bulkBody[2]).toStrictEqual({ index: { _index: createdIndex } });
    expect(bulkBody[3]).toStrictEqual(documents[1]);

    // Alias should have been rotated.
    expect(aliasesSpy).toHaveBeenCalled();
    const aliasActions = aliasesSpy.mock.calls[0][0].body.actions;
    expect(aliasActions[0]).toStrictEqual({
      remove: { index: 'some-type-index__*', alias: 'some-type-index__search' },
    });
    expect(aliasActions[1]).toStrictEqual({
      add: {
        indices: [
          'some-type-index__123tobedeleted',
          'some-type-index__456tobedeleted',
        ],
        alias: 'some-type-index__search_removable',
      },
    });
    expect(aliasActions[2]).toStrictEqual({
      add: { index: createdIndex, alias: 'some-type-index__search' },
    });

    // Old index should be cleaned up.
    expect(deleteSpy).toHaveBeenCalled();
  });

  it('handles bulk and batching during indexing', async () => {
    const documents = range(550).map(i => ({
      title: `Hello World ${i}`,
      location: `location-${i}`,
      // Generate large document sizes to trigger ES bulk flushing.
      text: range(2000).join(', '),
    }));

    await TestPipeline.withSubject(indexer).withDocuments(documents).execute();

    // Ensure multiple bulk requests were made.
    expect(bulkSpy).toHaveBeenCalledTimes(2);

    // Ensure the first and last documents were included in the payloads.
    const docLocations: string[] = [
      ...bulkSpy.mock.calls[0][0].body.map((l: any) => l.location),
      ...bulkSpy.mock.calls[1][0].body.map((l: any) => l.location),
    ];
    expect(docLocations).toContain('location-0');
    expect(docLocations).toContain('location-549');
  });

  it('ignores cleanup when no existing indices exist', async () => {
    const documents = [
      {
        title: 'testTerm',
        text: 'testText',
        location: 'test/location',
      },
    ];

    // Update initial alias cat to return nothing.
    catSpy = jest.fn().mockReturnValue([]);
    mock.clear({
      method: 'GET',
      path: '/_cat/aliases/some-type-index__search%2Csome-type-index__search_removable',
    });
    mock.add(
      {
        method: 'GET',
        path: '/_cat/aliases/some-type-index__search%2Csome-type-index__search_removable',
      },
      catSpy,
    );

    await TestPipeline.withSubject(indexer).withDocuments(documents).execute();

    // Final deletion shouldn't be called.
    expect(deleteSpy).not.toHaveBeenCalled();
  });
});
Example #25
Source File: heatmap.service.ts    From office-hours with GNU General Public License v3.0 4 votes vote down vote up
// PRIVATE function that is public for testing purposes
  // Rewind through the last few weeks and for each time interval,
  // figure out how long wait time would have been if you had joined the queue at that time
  // Timezone should be IANA
  // Returns heatmap in the timezone (ie 3rd bucket is 3am in that timezone)
  _generateHeatMapWithReplay(
    questions: QuestionModel[],
    hourTimestamps: [number, number][],
    timezone: string,
    bucketSize: number,
    samplesPerBucket: number,
  ): Heatmap {
    const sampleInterval = bucketSize / samplesPerBucket;
    /*
    TEST: Question1 is  3:05 - 3:25
    // The next question is 3:21 - 3:49
    THe following question is 4:05 - 4:10
    
    Bucket = 60, Samples = 3, so timepoints are: 3:00, 3:20, 3:40.

    3:20 sample gets waittime of 5 minutes
    3:40 samples get waittimes of 9 minutes
    4:00 sample gets waittime of 0 minutes


    If i entered the queue at that time when should I have gotten help?
    Every interval of minutes for the past 5 weeks are aggregated (by taking the avg)
    
    analyze the buckets to find the closest time approximation

    look at question Q1 and the next question Q2
    for all sample timepoints between Q1.createdAt and Q2.createdAt:
       - sample = Q1.helpedAt - timepoint (if negative, then it's 0)

  */

    function dateToBucket(date: Date | number): number {
      // parse in zone to handle daylight savings by getting day/hour/minute within that IANA zone
      const cInZone = moment.tz(date, timezone);
      return Math.floor(
        (cInZone.day() * 24 * 60 + cInZone.hour() * 60 + cInZone.minute()) /
          bucketSize,
      );
    }
    const timepointBuckets: number[][] = [
      ...Array((24 * 7 * 60) / bucketSize),
    ].map(() => []);

    if (questions.length) {
      const startDate = questions[0].createdAt;
      const sunday = moment.tz(startDate, timezone).startOf('week').toDate();

      function getNextTimepointIndex(date: Date): number {
        return Math.floor(timeDiffInMins(date, sunday) / sampleInterval) + 1;
      }

      // Get the date of the sample timepoint immediately after the given date
      function getNextSampleTimepoint(date: Date): Date {
        const timepointIndex = getNextTimepointIndex(date);
        return new Date(
          sunday.getTime() + timepointIndex * sampleInterval * 60 * 1000,
        );
      }

      // Get all timepoints between the two dates
      function getSampleTimepointsInDateRange(
        date1: Date,
        date2: Date,
      ): Date[] {
        const ret = [];
        let curr = getNextSampleTimepoint(date1);
        while (curr.getTime() < date2.getTime()) {
          ret.push(curr);
          curr = getNextSampleTimepoint(curr);
        }
        return ret;
      }

      // Get the start time of the current bucket
      function lastBucketBoundary(date: Date): moment.Moment {
        const startOfWeek = moment.tz(date, timezone).startOf('week');
        const m = moment(date);
        return m.subtract(m.diff(startOfWeek, 'm') % bucketSize, 'm');
      }

      // go two questions at a time
      let isFirst = true;
      for (let i = 0; i < questions.length; i++) {
        const curr = questions[i];
        const next = questions[i + 1];
        const isLast = i === questions.length - 1;

        // get the timepoints in between
        let sampledTimepoints = getSampleTimepointsInDateRange(
          isFirst
            ? lastBucketBoundary(curr.createdAt)
                .subtract(1, 's') // so that we get the first timepoint
                .toDate()
            : curr.createdAt,
          isLast
            ? lastBucketBoundary(curr.helpedAt)
                .add(bucketSize, 'm') // to get the nextBucketBoundary
                .toDate()
            : next.createdAt,
        );
        sampledTimepoints = sampledTimepoints.filter((time) =>
          hourTimestamps.some(([start, end]) =>
            inRange(time.getTime(), start, end),
          ),
        );

        // Pad the first bucket with zeros to account for timepoints before the first
        if (sampledTimepoints.length > 0 && isFirst) {
          isFirst = false;
        }
        // When we would have hypothetically gotten help at this timepoint
        for (const c of sampledTimepoints) {
          let wait = 0;
          if (
            inRange(
              c.getTime(),
              curr.createdAt.getTime(),
              curr.helpedAt.getTime(),
            )
          ) {
            wait = (curr.helpedAt.getTime() - c.getTime()) / 60000;
          }

          const bucketIndex = dateToBucket(c);
          timepointBuckets[bucketIndex].push(wait);
        }
      }
    }

    // Were there ever office hours in this bucket?
    const wereHoursDuringBucket: boolean[] = [
      ...Array((24 * 7 * 60) / bucketSize),
    ];
    for (const [start, end] of hourTimestamps) {
      //prevents an office hour from [N, M] to register in multiple buckets
      for (const i of range(dateToBucket(start), dateToBucket(end - 1) + 1)) {
        wereHoursDuringBucket[i] = true;
      }
    }

    const h: Heatmap = timepointBuckets.map((samples, i) => {
      if (samples.length > 0) {
        return mean(samples);
      } else if (wereHoursDuringBucket[i]) {
        return 0;
      } else {
        return -1;
      }
    });
    return h;
  }
Example #26
Source File: index.test.ts    From analytics-next with MIT License 4 votes vote down vote up
describe('Event Factory', () => {
  let user: User
  let factory: EventFactory

  const shoes = { product: 'shoes', total: '$35', category: 'category' }
  const shopper = { totalSpent: 100 }

  beforeEach(() => {
    user = new User()
    user.reset()
    factory = new EventFactory(user)
  })

  describe('alias', () => {
    test('creates alias events', () => {
      const alias = factory.alias('netto', 'netto farah')

      expect(alias.type).toEqual('alias')
      expect(alias.event).toBeUndefined()

      expect(alias.userId).toEqual('netto')
      expect(alias.previousId).toEqual('netto farah')
    })

    it('does not accept traits or properties', () => {
      const alias = factory.alias('netto', 'netto farah')
      expect(alias.traits).toBeUndefined()
      expect(alias.properties).toBeUndefined()
    })
  })

  describe('group', () => {
    test('creates group events', () => {
      const group = factory.group('userId', { coolkids: true })

      expect(group.traits).toEqual({ coolkids: true })
      expect(group.type).toEqual('group')
      expect(group.event).toBeUndefined()
    })

    it('accepts traits', () => {
      const group = factory.group('netto', shopper)
      expect(group.traits).toEqual(shopper)
    })

    it('sets the groupId to the message', () => {
      const group = factory.group('coolKidsId', { coolkids: true })
      expect(group.groupId).toEqual('coolKidsId')
    })
  })

  describe('page', () => {
    test('creates page events', () => {
      const page = factory.page('category', 'name')
      expect(page.traits).toBeUndefined()
      expect(page.type).toEqual('page')
      expect(page.event).toBeUndefined()
      expect(page.name).toEqual('name')
      expect(page.category).toEqual('category')
    })

    it('accepts properties', () => {
      const page = factory.page('category', 'name', shoes)
      expect(page.properties).toEqual(shoes)
    })

    it('ignores category and page if not passed in', () => {
      const page = factory.page(null, null)
      expect(page.category).toBeUndefined()
      expect(page.name).toBeUndefined()
    })
  })

  describe('identify', () => {
    test('creates identify events', () => {
      const identify = factory.identify('Netto', shopper)
      expect(identify.traits).toEqual(shopper)
      expect(identify.properties).toBeUndefined()
      expect(identify.type).toEqual('identify')
      expect(identify.event).toBeUndefined()
    })
  })

  describe('track', () => {
    test('creates track events', () => {
      const track = factory.track('Order Completed', shoes)
      expect(track.event).toEqual('Order Completed')
      expect(track.properties).toEqual(shoes)
      expect(track.traits).toBeUndefined()
      expect(track.type).toEqual('track')
    })

    test('adds a message id', () => {
      const track = factory.track('Order Completed', shoes)
      expect(track.messageId).toContain('ajs-next')
    })

    test('adds a random message id even when random is mocked', () => {
      jest.useFakeTimers()
      jest.spyOn(uuid, 'v4').mockImplementation(() => 'abc-123')
      // fake timer and fake uuid => equal
      expect(factory.track('Order Completed', shoes).messageId).toEqual(
        factory.track('Order Completed', shoes).messageId
      )

      // restore uuid function => not equal
      jest.restoreAllMocks()
      expect(factory.track('Order Completed', shoes).messageId).not.toEqual(
        factory.track('Order Completed', shoes).messageId
      )

      // restore timers function => not equal
      jest.useRealTimers()

      expect(factory.track('Order Completed', shoes).messageId).not.toEqual(
        factory.track('Order Completed', shoes).messageId
      )
    })

    test('message ids are random', () => {
      const ids = range(0, 200).map(
        () => factory.track('Order Completed', shoes).messageId
      )

      expect(uniq(ids)).toHaveLength(200)
    })

    test('sets an user id', () => {
      user.identify('007')

      const track = factory.track('Order Completed', shoes)
      expect(track.userId).toEqual('007')
    })

    test('sets an anonymous id', () => {
      const track = factory.track('Order Completed', shoes)
      expect(track.userId).toBeUndefined()
      expect(track.anonymousId).toEqual(user.anonymousId())
    })

    test('sets options in the context', () => {
      const track = factory.track('Order Completed', shoes, {
        opt1: true,
      })
      expect(track.context).toEqual({ opt1: true })
    })

    test('sets integrations', () => {
      const track = factory.track(
        'Order Completed',
        shoes,
        {},
        {
          amplitude: false,
        }
      )

      expect(track.integrations).toEqual({ amplitude: false })
    })

    test('merges integrations from `options` and `integrations`', () => {
      const track = factory.track(
        'Order Completed',
        shoes,
        {
          opt1: true,
          integrations: {
            amplitude: false,
          },
        },
        {
          googleAnalytics: true,
          amplitude: true,
        }
      )

      expect(track.integrations).toEqual({
        googleAnalytics: true,
        amplitude: false,
      })
    })

    test('do not send integration settings overrides from initialization', () => {
      const track = factory.track(
        'Order Completed',
        shoes,
        {
          integrations: {
            Amplitude: {
              sessionId: 'session_123',
            },
          },
        },
        {
          'Segment.io': {
            apiHost: 'custom',
          },
          GoogleAnalytics: false,

          'Customer.io': {},
        }
      )

      expect(track.integrations).toEqual({
        // do not pass Segment.io global settings
        'Segment.io': true,
        // accept amplitude event level settings
        Amplitude: {
          sessionId: 'session_123',
        },
        // pass along google analytics setting
        GoogleAnalytics: false,
        // empty objects are still valid
        'Customer.io': true,
      })
    })

    test('should move foreign options into `context`', () => {
      const track = factory.track('Order Completed', shoes, {
        opt1: true,
        opt2: '?',
        integrations: {
          amplitude: false,
        },
      })

      expect(track.context).toEqual({ opt1: true, opt2: '?' })
    })

    test('should not move known options into `context`', () => {
      const track = factory.track('Order Completed', shoes, {
        opt1: true,
        opt2: '?',
        integrations: {
          amplitude: false,
        },
        anonymousId: 'anon-1',
        timestamp: new Date(),
      })

      expect(track.context).toEqual({ opt1: true, opt2: '?' })
    })

    test('accepts an anonymous id', () => {
      const track = factory.track('Order Completed', shoes, {
        anonymousId: 'anon-1',
      })

      expect(track.context).toEqual({})
      expect(track.anonymousId).toEqual('anon-1')
    })

    test('accepts a timestamp', () => {
      const timestamp = new Date()
      const track = factory.track('Order Completed', shoes, {
        timestamp,
      })

      expect(track.context).toEqual({})
      expect(track.timestamp).toEqual(timestamp)
    })

    test('accepts traits', () => {
      const track = factory.track('Order Completed', shoes, {
        traits: {
          cell: '?',
        },
      })

      expect(track.context?.traits).toEqual({
        cell: '?',
      })
    })

    test('accepts a context object', () => {
      const track = factory.track('Order Completed', shoes, {
        context: {
          library: {
            name: 'ajs-next',
            version: '0.1.0',
          },
        },
      })

      expect(track.context).toEqual({
        library: {
          name: 'ajs-next',
          version: '0.1.0',
        },
      })
    })

    test('merges a context object', () => {
      const track = factory.track('Order Completed', shoes, {
        foreignProp: '??',
        context: {
          innerProp: '?',
          library: {
            name: 'ajs-next',
            version: '0.1.0',
          },
        },
      })

      expect(track.context).toEqual({
        library: {
          name: 'ajs-next',
          version: '0.1.0',
        },
        foreignProp: '??',
        innerProp: '?',
      })
    })
  })

  describe('normalize', function () {
    const msg: SegmentEvent = { type: 'track' }
    const opts: Options = (msg.options = {})

    describe('message', function () {
      it('should merge original with normalized', function () {
        msg.userId = 'user-id'
        opts.integrations = { Segment: true }
        const normalized = factory['normalize'](msg)

        expect(normalized.messageId?.length).toBeGreaterThanOrEqual(41) // 'ajs-next-md5(content + [UUID])'
        delete normalized.messageId

        expect(normalized).toStrictEqual({
          integrations: { Segment: true },
          type: 'track',
          userId: 'user-id',
          context: {},
        })
      })
    })
  })
})
Example #27
Source File: brush-selection-spec.ts    From S2 with MIT License 4 votes vote down vote up
describe('Interaction Brush Selection Tests', () => {
  let brushSelectionInstance: BrushSelection;
  let mockSpreadSheetInstance: SpreadSheet;
  let mockRootInteraction: RootInteraction;

  const startBrushDataCellMeta: Partial<ViewMeta> = {
    colIndex: 0,
    rowIndex: 1,
  };
  const endBrushDataCellMeta: Partial<ViewMeta> = {
    colIndex: 4,
    rowIndex: 3,
  };

  const startBrushDataCell = new MockDataCell();
  startBrushDataCell.getMeta = () => startBrushDataCellMeta as ViewMeta;

  const endBrushDataCell = new MockDataCell();
  endBrushDataCell.getMeta = () => endBrushDataCellMeta as ViewMeta;

  const panelGroupAllDataCells = Array.from<number[]>({ length: 4 })
    .fill(range(10))
    .reduce<DataCell[]>((arr, v, i) => {
      v.forEach((_, j) => {
        const cell = {
          cellType: CellTypes.DATA_CELL,
          getMeta() {
            return {
              colIndex: j,
              rowIndex: i,
            } as ViewMeta;
          },
        } as DataCell;
        arr.push(cell);
      });
      return arr;
    }, []);

  const emitEvent = (type: S2Event, event: Partial<OriginalEvent>) => {
    brushSelectionInstance.spreadsheet.emit(type, {
      originalEvent: event,
      preventDefault() {},
    } as any);
  };

  const emitGlobalEvent = (type: S2Event, event: Partial<MouseEvent>) => {
    brushSelectionInstance.spreadsheet.emit(type, {
      ...event,
      preventDefault() {},
    } as any);
  };

  beforeEach(() => {
    MockRootInteraction.mockClear();

    mockSpreadSheetInstance = new PivotSheet(
      document.createElement('div'),
      null,
      null,
    );
    mockRootInteraction = new MockRootInteraction(mockSpreadSheetInstance);
    mockSpreadSheetInstance.getCell = jest.fn(() => startBrushDataCell) as any;
    mockSpreadSheetInstance.foregroundGroup = new Group('');
    mockSpreadSheetInstance.showTooltipWithInfo = jest.fn();
    mockRootInteraction.getPanelGroupAllDataCells = () =>
      panelGroupAllDataCells;
    mockSpreadSheetInstance.interaction = mockRootInteraction;
    mockSpreadSheetInstance.render();
    mockSpreadSheetInstance.facet.layoutResult.colLeafNodes = Array.from(
      new Array(10),
    ).map((_, idx) => {
      return {
        colIndex: idx,
        id: idx,
        x: idx * 100,
        width: 100,
      };
    }) as unknown[] as Node[];
    mockSpreadSheetInstance.facet.layoutResult.rowLeafNodes = Array.from(
      new Array(10),
    ).map((_, idx) => {
      return {
        rowIndex: idx,
        id: idx,
        y: idx * 100,
        height: 100,
      };
    }) as unknown[] as Node[];
    mockSpreadSheetInstance.facet.getCellRange = () => {
      return {
        start: 0,
        end: 9,
      };
    };
    brushSelectionInstance = new BrushSelection(mockSpreadSheetInstance);
    brushSelectionInstance.brushSelectionStage =
      InteractionBrushSelectionStage.UN_DRAGGED;
    brushSelectionInstance.hidePrepareSelectMaskShape = jest.fn();
  });

  test('should register events', () => {
    expect(brushSelectionInstance.bindEvents).toBeDefined();
  });

  test('should not render invisible prepare select mask shape after rendered', () => {
    expect(brushSelectionInstance.prepareSelectMaskShape).not.toBeDefined();
  });

  test('should init brush selection stage', () => {
    expect(brushSelectionInstance.brushSelectionStage).toEqual(
      InteractionBrushSelectionStage.UN_DRAGGED,
    );
  });

  test('should render invisible prepare select mask shape after mouse down on the data cell', () => {
    emitEvent(S2Event.DATA_CELL_MOUSE_DOWN, {
      layerX: 10,
      layerY: 20,
    });
    expect(brushSelectionInstance.prepareSelectMaskShape).toBeDefined();
    expect(
      brushSelectionInstance.prepareSelectMaskShape.attr('visible'),
    ).toBeFalsy();
  });

  test('should get start brush point when mouse down', () => {
    emitEvent(S2Event.DATA_CELL_MOUSE_DOWN, {
      layerX: 10,
      layerY: 20,
    });
    expect(brushSelectionInstance.spreadsheet.getCell).toHaveBeenCalled();
    expect(brushSelectionInstance.startBrushPoint).toStrictEqual({
      x: 10,
      y: 20,
      scrollX: 0,
      scrollY: 0,
      rowIndex: 1,
      colIndex: 0,
    });
    expect(brushSelectionInstance.displayedDataCells).toEqual(
      panelGroupAllDataCells,
    );
  });

  test('should skip brush selection if mouse not dragged', () => {
    emitEvent(S2Event.DATA_CELL_MOUSE_DOWN, {
      layerX: 10,
      layerY: 20,
    });
    emitGlobalEvent(S2Event.GLOBAL_MOUSE_MOVE, {
      clientX: 12,
      clientY: 22,
    });
    emitEvent(S2Event.GLOBAL_MOUSE_UP, {});

    expect(brushSelectionInstance.brushSelectionStage).toEqual(
      InteractionBrushSelectionStage.UN_DRAGGED,
    );
    expect(
      brushSelectionInstance.spreadsheet.interaction.hasIntercepts([
        InterceptType.BRUSH_SELECTION,
      ]),
    ).toBeFalsy();

    // 如果刷选距离过短, 则走单选的逻辑, 需要隐藏刷选提示框
    expect(
      brushSelectionInstance.hidePrepareSelectMaskShape,
    ).toHaveBeenCalled();
  });

  // https://github.com/antvis/S2/issues/852
  test('should clear brush selection state when mouse down and context menu clicked', () => {
    const globalMouseUp = jest.fn();
    mockSpreadSheetInstance.on(S2Event.GLOBAL_MOUSE_UP, globalMouseUp);

    emitEvent(S2Event.DATA_CELL_MOUSE_DOWN, {
      layerX: 10,
      layerY: 20,
    });
    emitGlobalEvent(S2Event.GLOBAL_MOUSE_MOVE, {
      clientX: 12,
      clientY: 22,
    });

    expect(brushSelectionInstance.brushSelectionStage).toEqual(
      InteractionBrushSelectionStage.DRAGGED,
    );

    emitEvent(S2Event.GLOBAL_CONTEXT_MENU, {});

    expect(globalMouseUp).not.toHaveBeenCalled();
    expect(brushSelectionInstance.brushSelectionStage).toEqual(
      InteractionBrushSelectionStage.UN_DRAGGED,
    );
    expect(
      brushSelectionInstance.spreadsheet.interaction.hasIntercepts([
        InterceptType.HOVER,
      ]),
    ).toBeFalsy();
    expect(
      brushSelectionInstance.spreadsheet.interaction.hasIntercepts([
        InterceptType.BRUSH_SELECTION,
      ]),
    ).toBeFalsy();
    expect(
      brushSelectionInstance.hidePrepareSelectMaskShape,
    ).toHaveReturnedTimes(1);
  });

  test('should skip brush selection if mouse move less than valid distance', () => {
    emitEvent(S2Event.GLOBAL_MOUSE_MOVE, {});

    expect(brushSelectionInstance.brushSelectionStage).toEqual(
      InteractionBrushSelectionStage.UN_DRAGGED,
    );
    expect(brushSelectionInstance.endBrushPoint).not.toBeDefined();
    expect(brushSelectionInstance.brushRangeDataCells).toHaveLength(0);
    expect(
      brushSelectionInstance.spreadsheet.interaction.hasIntercepts([
        InterceptType.HOVER,
      ]),
    ).toBeFalsy();
  });

  test('should get brush selection range cells', () => {
    const selectedFn = jest.fn();
    const brushSelectionFn = jest.fn();

    mockSpreadSheetInstance.getCell = jest.fn(() => startBrushDataCell) as any;

    mockSpreadSheetInstance.on(S2Event.GLOBAL_SELECTED, selectedFn);
    mockSpreadSheetInstance.on(
      S2Event.DATE_CELL_BRUSH_SELECTION,
      brushSelectionFn,
    );

    // ================== mouse down ==================
    emitEvent(S2Event.DATA_CELL_MOUSE_DOWN, { layerX: 10, layerY: 20 });

    mockSpreadSheetInstance.getCell = jest.fn(() => endBrushDataCell) as any;
    // ================== mouse move ==================
    emitGlobalEvent(S2Event.GLOBAL_MOUSE_MOVE, {
      clientX: 100,
      clientY: 200,
    });

    expect(brushSelectionInstance.brushSelectionStage).toEqual(
      InteractionBrushSelectionStage.DRAGGED,
    );
    // get end brush point
    expect(brushSelectionInstance.endBrushPoint).toEqual({
      ...endBrushDataCellMeta,
      x: 100,
      y: 200,
    });
    // show prepare brush selection mask
    expect(brushSelectionInstance.prepareSelectMaskShape.attr()).toMatchObject({
      x: 10,
      y: 20,
      width: 90,
      height: 180,
    });

    // ================== mouse up ==================
    emitEvent(S2Event.GLOBAL_MOUSE_UP, {});
    // hide prepare mask
    expect(
      brushSelectionInstance.prepareSelectMaskShape.attr('visible'),
    ).toBeFalsy();
    // show tooltip
    expect(mockSpreadSheetInstance.showTooltipWithInfo).toHaveBeenCalled();
    // reset brush stage
    expect(brushSelectionInstance.brushSelectionStage).toEqual(
      InteractionBrushSelectionStage.UN_DRAGGED,
    );
    // get brush range selected cells
    expect(brushSelectionInstance.brushRangeDataCells).toHaveLength(15);
    brushSelectionInstance.brushRangeDataCells.forEach((cell) => {
      const { rowIndex, colIndex } = cell.getMeta();
      expect(rowIndex).toBeLessThanOrEqual(endBrushDataCellMeta.rowIndex);
      expect(rowIndex).toBeGreaterThanOrEqual(startBrushDataCellMeta.rowIndex);
      expect(colIndex).toBeLessThanOrEqual(endBrushDataCellMeta.colIndex);
      expect(colIndex).toBeGreaterThanOrEqual(startBrushDataCellMeta.colIndex);
    });
    // emit event
    expect(selectedFn).toHaveBeenCalledTimes(1);
    expect(brushSelectionFn).toHaveBeenCalledTimes(1);
  });

  test('should get correct formatted brush point', () => {
    const EXTRA_PIXEL = 2;
    const VSCROLLBAR_WIDTH = 5;
    const { width, height } = mockSpreadSheetInstance.facet.getCanvasHW();
    const minX = 10;
    const minY = 10;
    const maxY = height + 10;
    const maxX = width + 10;
    mockSpreadSheetInstance.facet.panelBBox = {
      minX,
      minY,
      maxY,
      maxX,
    } as any;
    brushSelectionInstance.endBrushPoint = {
      x: maxX - 10,
      y: maxY - 10,
      scrollX: 0,
      colIndex: 0,
      rowIndex: 0,
    };
    mockSpreadSheetInstance.facet.vScrollBar = {
      getBBox: () =>
        ({
          width: VSCROLLBAR_WIDTH,
        } as any),
    } as any;
    let result = brushSelectionInstance.formatBrushPointForScroll({
      x: 20,
      y: 20,
    });

    expect(result).toStrictEqual({
      x: {
        needScroll: true,
        value: maxX - VSCROLLBAR_WIDTH - EXTRA_PIXEL,
      },
      y: {
        needScroll: true,
        value: maxY - EXTRA_PIXEL,
      },
    });

    brushSelectionInstance.endBrushPoint = {
      x: maxX - 10,
      y: maxY - 10,
      scrollX: 0,
      colIndex: 0,
      rowIndex: 0,
    };

    result = brushSelectionInstance.formatBrushPointForScroll({
      x: 1,
      y: 1,
    });

    expect(result).toStrictEqual({
      x: {
        needScroll: false,
        value: maxX - 10 + 1,
      },
      y: {
        needScroll: false,
        value: maxY - 10 + 1,
      },
    });

    brushSelectionInstance.endBrushPoint = {
      x: minX + 10,
      y: minY + 10,
      scrollX: 0,
      colIndex: 0,
      rowIndex: 0,
    };

    result = brushSelectionInstance.formatBrushPointForScroll({
      x: -20,
      y: -20,
    });

    expect(result).toStrictEqual({
      x: {
        needScroll: true,
        value: minX + EXTRA_PIXEL,
      },
      y: {
        needScroll: true,
        value: minY + EXTRA_PIXEL,
      },
    });
  });

  test('should get correct selected cell metas', () => {
    expect(
      brushSelectionInstance.getSelectedCellMetas({
        start: {
          colIndex: 0,
          rowIndex: 0,
        },
        end: {
          colIndex: 9,
          rowIndex: 9,
        },
      } as any).length,
    ).toBe(100);
  });

  test('should get correct adjusted frozen rowIndex and colIndex', () => {
    const { adjustNextColIndexWithFrozen, adjustNextRowIndexWithFrozen } =
      brushSelectionInstance;
    mockSpreadSheetInstance.setOptions({
      frozenColCount: 1,
      frozenRowCount: 1,
      frozenTrailingColCount: 1,
      frozenTrailingRowCount: 1,
    });
    mockSpreadSheetInstance.dataSet.getDisplayDataSet = () => {
      return Array.from(new Array(10)).map(() => {
        return {};
      });
    };
    (mockSpreadSheetInstance.facet as TableFacet).panelScrollGroupIndexes = [
      1, 8, 1, 8,
    ];

    expect(adjustNextColIndexWithFrozen(9, ScrollDirection.TRAILING)).toBe(8);
    expect(adjustNextColIndexWithFrozen(0, ScrollDirection.LEADING)).toBe(1);
    expect(adjustNextColIndexWithFrozen(7, ScrollDirection.TRAILING)).toBe(7);

    expect(adjustNextRowIndexWithFrozen(9, ScrollDirection.TRAILING)).toBe(8);
    expect(adjustNextRowIndexWithFrozen(0, ScrollDirection.LEADING)).toBe(1);
    expect(adjustNextRowIndexWithFrozen(7, ScrollDirection.TRAILING)).toBe(7);
  });

  test('should get correct scroll offset for row and col', () => {
    const { facet } = mockSpreadSheetInstance;
    expect(
      getScrollOffsetForCol(
        7,
        ScrollDirection.LEADING,
        mockSpreadSheetInstance,
      ),
    ).toBe(700);
    expect(
      getScrollOffsetForCol(
        7,
        ScrollDirection.TRAILING,
        mockSpreadSheetInstance,
      ),
    ).toBe(200);

    (facet as TableFacet).frozenGroupInfo = {
      [FrozenGroup.FROZEN_COL]: {
        width: 100,
      },
      [FrozenGroup.FROZEN_TRAILING_COL]: {
        width: 100,
      },
      [FrozenGroup.FROZEN_ROW]: {
        height: 0,
      },
      [FrozenGroup.FROZEN_TRAILING_ROW]: {
        height: 0,
      },
    };

    expect(
      getScrollOffsetForCol(
        7,
        ScrollDirection.LEADING,
        mockSpreadSheetInstance,
      ),
    ).toBe(600);
    expect(
      getScrollOffsetForCol(
        7,
        ScrollDirection.TRAILING,
        mockSpreadSheetInstance,
      ),
    ).toBe(300);

    facet.panelBBox = {
      height: facet.getCanvasHW().height,
    } as any;

    facet.viewCellHeights = facet.getViewCellHeights(facet.layoutResult);

    expect(
      getScrollOffsetForRow(
        7,
        ScrollDirection.LEADING,
        mockSpreadSheetInstance,
      ),
    ).toBe(700);
    expect(
      getScrollOffsetForRow(
        7,
        ScrollDirection.TRAILING,
        mockSpreadSheetInstance,
      ),
    ).toBe(320);

    (facet as TableFacet).frozenGroupInfo = {
      [FrozenGroup.FROZEN_COL]: {
        width: 0,
      },
      [FrozenGroup.FROZEN_TRAILING_COL]: {
        width: 0,
      },
      [FrozenGroup.FROZEN_ROW]: {
        height: 100,
      },
      [FrozenGroup.FROZEN_TRAILING_ROW]: {
        height: 100,
      },
    };
    expect(
      getScrollOffsetForRow(
        7,
        ScrollDirection.LEADING,
        mockSpreadSheetInstance,
      ),
    ).toBe(600);
    expect(
      getScrollOffsetForRow(
        7,
        ScrollDirection.TRAILING,
        mockSpreadSheetInstance,
      ),
    ).toBe(420);
  });

  test('should get valid x and y index', () => {
    const { validateXIndex, validateYIndex } = brushSelectionInstance;
    expect(validateXIndex(-1)).toBe(null);
    expect(validateXIndex(1)).toBe(1);
    expect(validateXIndex(10)).toBe(null);
    expect(validateXIndex(9)).toBe(9);

    expect(validateYIndex(-1)).toBe(null);
    expect(validateYIndex(1)).toBe(1);
    expect(validateYIndex(10)).toBe(null);
    expect(validateYIndex(9)).toBe(9);

    (mockSpreadSheetInstance.facet as TableFacet).frozenGroupInfo = {
      [FrozenGroup.FROZEN_COL]: {
        range: [0, 1],
      },
      [FrozenGroup.FROZEN_TRAILING_COL]: {
        range: [8, 9],
      },
      [FrozenGroup.FROZEN_ROW]: {
        range: [0, 1],
      },
      [FrozenGroup.FROZEN_TRAILING_ROW]: {
        range: [8, 9],
      },
    };

    expect(validateXIndex(1)).toBe(null);
    expect(validateXIndex(2)).toBe(2);
    expect(validateXIndex(8)).toBe(null);
    expect(validateXIndex(7)).toBe(7);
    expect(validateXIndex(1)).toBe(null);
    expect(validateXIndex(2)).toBe(2);
    expect(validateXIndex(8)).toBe(null);
    expect(validateXIndex(7)).toBe(7);
  });
});
Example #28
Source File: LogRows.test.tsx    From grafana-chinese with Apache License 2.0 4 votes vote down vote up
describe('LogRows', () => {
  it('renders rows', () => {
    const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })];
    const wrapper = mount(
      <LogRows
        logRows={rows}
        dedupStrategy={LogsDedupStrategy.none}
        highlighterExpressions={[]}
        showLabels={false}
        showTime={false}
        wrapLogMessage={true}
        timeZone={'utc'}
      />
    );

    expect(wrapper.find(LogRow).length).toBe(3);
    expect(wrapper.contains('log message 1')).toBeTruthy();
    expect(wrapper.contains('log message 2')).toBeTruthy();
    expect(wrapper.contains('log message 3')).toBeTruthy();
  });

  it('renders rows only limited number of rows first', () => {
    const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })];
    jest.useFakeTimers();
    const wrapper = mount(
      <LogRows
        logRows={rows}
        dedupStrategy={LogsDedupStrategy.none}
        highlighterExpressions={[]}
        showLabels={false}
        showTime={false}
        wrapLogMessage={true}
        timeZone={'utc'}
        previewLimit={1}
      />
    );

    expect(wrapper.find(LogRow).length).toBe(1);
    expect(wrapper.contains('log message 1')).toBeTruthy();
    jest.runAllTimers();
    wrapper.update();

    expect(wrapper.find(LogRow).length).toBe(3);
    expect(wrapper.contains('log message 1')).toBeTruthy();
    expect(wrapper.contains('log message 2')).toBeTruthy();
    expect(wrapper.contains('log message 3')).toBeTruthy();

    jest.useRealTimers();
  });

  it('renders deduped rows if supplied', () => {
    const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })];
    const dedupedRows: LogRowModel[] = [makeLog({ uid: '4' }), makeLog({ uid: '5' })];
    const wrapper = mount(
      <LogRows
        logRows={rows}
        deduplicatedRows={dedupedRows}
        dedupStrategy={LogsDedupStrategy.none}
        highlighterExpressions={[]}
        showLabels={false}
        showTime={false}
        wrapLogMessage={true}
        timeZone={'utc'}
      />
    );

    expect(wrapper.find(LogRow).length).toBe(2);
    expect(wrapper.contains('log message 4')).toBeTruthy();
    expect(wrapper.contains('log message 5')).toBeTruthy();
  });

  it('renders with default preview limit', () => {
    // PREVIEW_LIMIT * 2 is there because otherwise we just render all rows
    const rows: LogRowModel[] = range(PREVIEW_LIMIT * 2 + 1).map(num => makeLog({ uid: num.toString() }));
    const wrapper = mount(
      <LogRows
        logRows={rows}
        dedupStrategy={LogsDedupStrategy.none}
        highlighterExpressions={[]}
        showLabels={false}
        showTime={false}
        wrapLogMessage={true}
        timeZone={'utc'}
      />
    );

    expect(wrapper.find(LogRow).length).toBe(100);
  });
});
Example #29
Source File: Sidebar.tsx    From project-papua with Apache License 2.0 4 votes vote down vote up
Sidebar: React.FC<Props> = (props) => {
  const { pages } = props
  const { translateByID, form, pageIndex, setPage, completion } = useContext(FormContext)
  const { language, setLanguage } = useContext(LanguageContext)
  const size = useContext(ResponsiveContext)

  const currentPage = pages[pageIndex]
  const percent = Math.floor((pageIndex / (pages.length - 1)) * 100)

  const canClickPage = (i: number) => {
    return range(0, i).every((index) => completion[index])
  }

  return (
    <Box
      flex={{ shrink: 0 }}
      margin={size === 'small' ? { top: 'small' } : { left: 'small' }}
      direction="column"
      width={size === 'small' ? '100%' : '350px'}
    >
      <Card margin={{ bottom: 'small' }} pad={{ horizontal: size === 'small' ? '24px' : 'medium', vertical: 'small' }}>
        <Markdown>{translateByID('demo-warning')}</Markdown>
      </Card>
      <Card pad="medium">
        {form.seal && (
          <Box margin={{ bottom: 'medium' }}>
            <Image src={form.seal} style={{ maxHeight: '175px', maxWidth: '100%', objectFit: 'contain' }} />
          </Box>
        )}
        <Box>
          <Heading level={4} margin="none">
            {translateByID('language')}
          </Heading>
          <StyledSelect
            a11yTitle="select language"
            margin={{ top: 'xsmall' }}
            options={languages}
            labelKey="title"
            valueKey={{ key: 'value', reduce: true }}
            value={language}
            onChange={({ value }) => setLanguage(value)}
          />
        </Box>
        <Box margin={{ top: 'medium' }}>
          <Box direction="row" justify="between">
            <Heading level={4} margin="none">
              {translateByID('progress')}
            </Heading>
            <Paragraph margin="none">{percent}%</Paragraph>
          </Box>
          <Box
            margin={{ top: 'xsmall' }}
            style={{ width: '100%', height: '12px', borderRadius: '12px', background: '#F2F2F2' }}
          >
            <Box style={{ width: `${percent}%`, height: '100%', borderRadius: '12px', background: '#3A80C2' }} />
          </Box>
        </Box>
        <Box margin={{ top: 'medium' }}>
          {size === 'small' && pages.length > 2 ? (
            <>
              {/* On small screens, we collapse the section titles to a Select */}
              <Heading level={4} margin="none">
                {translateByID('section')}
              </Heading>
              <StyledSelect
                a11yTitle="select section"
                margin={{ top: 'xsmall' }}
                options={pages.map((page, i) => ({ page, i, disabled: !canClickPage(i) }))}
                labelKey="page"
                valueKey={{ key: 'i', reduce: true }}
                disabledKey="disabled"
                value={pageIndex as any} /* These type definitions don't support values as numbers */
                // TODO: In production, add a `canClickPage(i) && ` below to prevent folks from jumping forward.
                onChange={({ value: i }) => setPage(i)}
              />
            </>
          ) : (
            /* On larger screens, we show all section titles as a list */
            pages.map((page, i) => {
              return (
                <Text
                  style={{
                    cursor: canClickPage(i) ? 'pointer' : 'not-allowed',
                    opacity: currentPage === page ? '100%' : canClickPage(i) ? '50%' : '20%',
                  }}
                  // TODO: In production, add a `canClickPage(i) && ` below to prevent folks from jumping forward.
                  onClick={() => setPage(i)}
                  margin={{ bottom: 'xsmall' }}
                  key={page}
                >
                  {page}
                </Text>
              )
            })
          )}
        </Box>
      </Card>
    </Box>
  )
}