recoil#useRecoilCallback TypeScript Examples

The following examples show how to use recoil#useRecoilCallback. 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: RecoilNexus.tsx    From recoil-nexus with MIT License 6 votes vote down vote up
export default function RecoilNexus() {

    nexus.get = useRecoilCallback<[atom: RecoilValue<any>], any>(({ snapshot }) =>
        function <T>(atom: RecoilValue<T>) {
            return snapshot.getLoadable(atom).contents
        }, [])

    nexus.getPromise = useRecoilCallback<[atom: RecoilValue<any>], Promise<any>>(({ snapshot }) =>
        function <T>(atom: RecoilValue<T>) {
            return snapshot.getPromise(atom)
        }, [])

    nexus.set = useRecoilCallback(({ set }) => set, [])

    nexus.reset = useRecoilCallback(({ reset }) => reset, [])

    return null
}
Example #2
Source File: useExportResults.ts    From nextclade with MIT License 6 votes vote down vote up
function useResultsExport(exportFn: (filename: string, snapshot: Snapshot, worker: ExportWorker) => Promise<void>) {
  return useRecoilCallback(
    ({ set, snapshot }) => {
      const snapshotRelease = snapshot.retain()
      return (filename: string) => {
        void ExportWorker.get()
          .then((worker) => exportFn(filename, snapshot, worker))
          .catch((error) => {
            set(globalErrorAtom, error)
          })
          .finally(() => {
            snapshotRelease()
          })
      }
    },
    [exportFn],
  )
}
Example #3
Source File: SequenceSelector.tsx    From nextclade with MIT License 5 votes vote down vote up
export function SequenceSelector() {
  const { t } = useTranslationSafe()

  const geneNames = useRecoilValue(geneNamesAtom)

  const viewedGene = useRecoilValue(viewedGeneAtom)
  const onChangeGene = useRecoilCallback(
    ({ set }) =>
      (e: React.ChangeEvent<HTMLSelectElement>) => {
        set(viewedGeneAtom, e.target.value)
      },
    [],
  )

  const getOptionText = useCallback(
    (gene: string) => {
      if (gene === GENE_OPTION_NUC_SEQUENCE) {
        return t('Nucleotide sequence')
      }

      return t('Gene {{geneName}}', { geneName: gene })
    },
    [t],
  )

  const geneOptions = useMemo(() => {
    return [GENE_OPTION_NUC_SEQUENCE, ...geneNames].map((gene) => {
      return (
        <option key={gene} value={gene}>
          {getOptionText(gene)}
        </option>
      )
    })
  }, [geneNames, getOptionText])

  return (
    <Select
      name="sequence-view-gene-dropdown"
      id="sequence-view-gene-dropdown"
      onChange={onChangeGene}
      value={viewedGene}
    >
      {geneOptions}
    </Select>
  )
}
Example #4
Source File: useRunAnalysis.ts    From nextclade with MIT License 5 votes vote down vote up
export function useRunAnalysis() {
  const router = useRouter()
  const dispatch = useDispatch()

  return useRecoilCallback(
    ({ set, reset, snapshot: { getPromise } }) =>
      () => {
        set(analysisStatusGlobalAtom, AlgorithmGlobalStatus.loadingData)
        set(showNewRunPopupAtom, false)

        reset(analysisResultsAtom)

        const numThreads = getPromise(numThreadsAtom)
        const datasetCurrent = getPromise(datasetCurrentAtom)
        const qrySeq = getPromise(qrySeqInputAtom)

        const inputs: LaunchAnalysisInputs = {
          ref_seq_str: getPromise(refSeqInputAtom),
          gene_map_str: getPromise(geneMapInputAtom),
          tree_str: getPromise(refTreeInputAtom),
          qc_config_str: getPromise(qcConfigInputAtom),
          virus_properties_str: getPromise(virusPropertiesInputAtom),
          pcr_primers_str: getPromise(primersCsvInputAtom),
        }

        const callbacks: LaunchAnalysisCallbacks = {
          onGlobalStatus(status) {
            set(analysisStatusGlobalAtom, status)
          },
          onInitialData({ geneMap, genomeSize, cladeNodeAttrKeyDescs }) {
            set(geneMapAtom, geneMap)
            set(genomeSizeAtom, genomeSize)
            set(cladeNodeAttrDescsAtom, cladeNodeAttrKeyDescs)
          },
          onParsedFasta(/* record */) {
            // TODO: this does not work well: updates in `onAnalysisResult()` callback below fight with this one.
            // Figure out how to make them both work.
            // set(analysisResultsAtom(record.seqName), { index: record.index, seqName: record.seqName })
          },
          onAnalysisResult(result) {
            set(analysisResultAtom(result.seqName), result)
          },
          onError(error) {
            set(globalErrorAtom, error)
          },
          onTree(tree: AuspiceJsonV2) {
            set(treeAtom, tree)

            const auspiceState = createAuspiceState(tree, dispatch)
            dispatch(auspiceStartClean(auspiceState))
            dispatch(changeColorBy())
            dispatch(treeFilterByNodeType(['New']))
          },
          onComplete() {},
        }

        router
          .push('/results', '/results')
          .then(async () => {
            set(analysisStatusGlobalAtom, AlgorithmGlobalStatus.initWorkers)
            return launchAnalysis(qrySeq, inputs, callbacks, datasetCurrent, numThreads)
          })
          .catch((error) => {
            set(analysisStatusGlobalAtom, AlgorithmGlobalStatus.failed)
            set(globalErrorAtom, sanitizeError(error))
          })
      },
    [router, dispatch],
  )
}
Example #5
Source File: _app.tsx    From nextclade with MIT License 4 votes vote down vote up
/**
 * Dummy component that allows to set recoil state asynchronously. Needed because RecoilRoot's initializeState
 * currently only handles synchronous update and any calls to set() from promises have no effect
 */
export function RecoilStateInitializer() {
  const router = useRouter()

  // NOTE: Do manual parsing, because router.query is randomly empty on the first few renders and on repeated renders.
  // This is important, because various states depend on query, and when it changes back and forth,
  // the state also changes unexpectedly.
  const { query: urlQuery } = useMemo(() => parseUrl(router.asPath), [router.asPath])

  const [initialized, setInitialized] = useRecoilState(isInitializedAtom)

  const run = useRunAnalysis()

  const error = useRecoilValue(globalErrorAtom)

  const initialize = useRecoilCallback(({ set, snapshot }) => () => {
    if (initialized) {
      return
    }

    const snapShotRelease = snapshot.retain()
    const { getPromise } = snapshot

    // eslint-disable-next-line no-void
    void initializeDatasets(urlQuery)
      .catch((error) => {
        // Dataset error is fatal and we want error to be handled in the ErrorBoundary
        setInitialized(false)
        throw error
      })
      .then(({ datasets, defaultDatasetName, defaultDatasetNameFriendly, currentDatasetName }) => {
        set(datasetsAtom, {
          datasets,
          defaultDatasetName,
          defaultDatasetNameFriendly,
        })
        set(datasetCurrentNameAtom, (previous) => currentDatasetName ?? previous)

        return undefined
      })
      .then(() => {
        const qrySeqInput = createInputFromUrlParamMaybe(urlQuery, 'input-fasta')
        set(qrySeqInputAtom, qrySeqInput)

        set(refSeqInputAtom, createInputFromUrlParamMaybe(urlQuery, 'input-root-seq'))
        set(geneMapInputAtom, createInputFromUrlParamMaybe(urlQuery, 'input-gene-map'))
        set(refTreeInputAtom, createInputFromUrlParamMaybe(urlQuery, 'input-tree'))
        set(qcConfigInputAtom, createInputFromUrlParamMaybe(urlQuery, 'input-qc-config'))
        set(virusPropertiesInputAtom, createInputFromUrlParamMaybe(urlQuery, 'input-pcr-primers'))
        set(primersCsvInputAtom, createInputFromUrlParamMaybe(urlQuery, 'input-virus-properties'))

        if (qrySeqInput) {
          run()
        }

        return undefined
      })
      .then(async () => {
        const changelogShouldShowOnUpdates = await getPromise(changelogShouldShowOnUpdatesAtom)
        const changelogLastVersionSeen = await getPromise(changelogLastVersionSeenAtom)
        set(changelogIsShownAtom, shouldShowChangelog(changelogLastVersionSeen, changelogShouldShowOnUpdates))
        set(changelogLastVersionSeenAtom, (prev) => process.env.PACKAGE_VERSION ?? prev ?? '')
        return undefined
      })
      .then(() => {
        setInitialized(true)
        return undefined
      })
      .catch((error) => {
        setInitialized(true)
        set(globalErrorAtom, sanitizeError(error))
      })
      .finally(() => {
        snapShotRelease()
      })
  })

  useEffect(() => {
    initialize()
  })

  if (!initialized && !isNil(error)) {
    throw error
  }

  return null
}
Example #6
Source File: index.spec.tsx    From recoil-persist with MIT License 4 votes vote down vote up
function testPersistWith(storage: TestableStorage) {
  describe(`Storage: ${storage.name}`, () => {
    const testKey = 'test-key'
    const { persistAtom } = recoilPersist({ key: testKey, storage })

    const getStateValue = () => {
      const value = storage.getState()[testKey]
      if (value == undefined) {
        return {}
      }
      return JSON.parse(value)
    }

    const getAtomKey = (key: string) => {
      return `${storage.name}_${key}`
    }

    const counterState = atom({
      key: getAtomKey('count'),
      default: 0,
      effects_UNSTABLE: [persistAtom],
    })

    const counterFamily = atomFamily({
      key: getAtomKey('countFamily'),
      default: 0,
      effects_UNSTABLE: [persistAtom],
    })

    const counterState4 = atom({
      key: getAtomKey('count4'),
      default: 0,
    })

    function Demo() {
      const [count, setCount] = useRecoilState(counterState)
      const [count2, setCount2] = useRecoilState(counterFamily('2'))
      const [count3, setCount3] = useRecoilState(counterFamily('3'))
      const [count4, setCount4] = useRecoilState(counterState4)
      const resetCounter3 = useResetRecoilState(counterFamily('3'))
      const updateMultiple = useRecoilCallback(({ set }) => () => {
        set(counterState, 10)
        set(counterFamily('2'), 10)
      })
      return (
        <div>
          <p data-testid="count-value">{count}</p>
          <p data-testid="count2-value">{count2}</p>
          <p data-testid="count3-value">{count3}</p>
          <p data-testid="count4-value">{count4}</p>
          <button
            data-testid="count-increase"
            onClick={() => setCount(count + 1)}
          >
            Increase
          </button>
          <button
            data-testid="count2-increase"
            onClick={() => setCount2(count2 + 1)}
          >
            Increase 2
          </button>
          <button
            data-testid="count3-increase"
            onClick={() => setCount3(count3 + 1)}
          >
            Increase 3
          </button>
          <button
            data-testid="count4-increase"
            onClick={() => setCount4(count4 + 1)}
          >
            Increase 4
          </button>
          <button
            data-testid="count3-null-value"
            onClick={() => setCount3(null)}
          >
            Set value to null
          </button>
          <button
            data-testid="count3-undefined-value"
            onClick={() => setCount3(undefined)}
          >
            Set value to undefined
          </button>
          <button data-testid="count3-reset" onClick={() => resetCounter3()}>
            Reset count 3
          </button>
          <button
            data-testid="update-multiple"
            onClick={() => updateMultiple()}
          >
            Update multiple
          </button>
        </div>
      )
    }

    beforeEach(() => {
      console.error = jest.fn()
    })

    afterEach(() => {
      storage.clear()
      jest.restoreAllMocks()
    })

    it('should be removed from storage on reset', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count3-increase'))
      await waitFor(() =>
        expect(getByTestId('count3-value').innerHTML).toBe('1'),
      )

      expect(getStateValue()).toStrictEqual({
        [getAtomKey('countFamily__"3"')]: 1,
      })

      fireEvent.click(getByTestId('count3-reset'))
      await waitFor(() =>
        expect(getByTestId('count3-value').innerHTML).toBe('0'),
      )

      expect(getStateValue()).toStrictEqual({})
    })

    it('should handle reset atom with default value', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count3-reset'))
      await waitFor(() =>
        expect(getByTestId('count3-value').innerHTML).toBe('0'),
      )

      expect(getStateValue()).toStrictEqual({})
    })

    it('should update storage with null', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count3-null-value'))
      await waitFor(() =>
        expect(getByTestId('count3-value').innerHTML).toBe(''),
      )

      expect(getStateValue()).toStrictEqual({
        [getAtomKey('countFamily__"3"')]: null,
      })
    })

    it('should update storage with undefined', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count3-undefined-value'))
      await waitFor(() =>
        expect(getByTestId('count3-value').innerHTML).toBe(''),
      )

      expect(getStateValue()).toStrictEqual({})
    })

    it('should update storage', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count-increase'))
      await waitFor(() =>
        expect(getByTestId('count-value').innerHTML).toBe('1'),
      )

      expect(getStateValue()).toStrictEqual({
        [getAtomKey('count')]: 1,
      })
    })

    it('should update storage if using atomFamily', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )
      fireEvent.click(getByTestId('count2-increase'))
      await waitFor(() =>
        expect(getByTestId('count2-value').innerHTML).toBe('1'),
      )
      fireEvent.click(getByTestId('count3-increase'))
      await waitFor(() =>
        expect(getByTestId('count3-value').innerHTML).toBe('1'),
      )

      expect(getStateValue()).toStrictEqual({
        [getAtomKey('countFamily__"2"')]: 1,
        [getAtomKey('countFamily__"3"')]: 1,
      })
    })

    it('should not persist atom with no effect', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count4-increase'))
      await waitFor(() =>
        expect(getByTestId('count4-value').innerHTML).toBe('1'),
      )

      expect(storage.getState()[testKey]).toBeUndefined()
    })

    it('should read state from storage', async () => {
      await storage.setItem(
        testKey,
        JSON.stringify({
          [getAtomKey('count')]: 1,
          [getAtomKey('countFamily__"2"')]: 1,
        }),
      )

      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      await waitFor(() =>
        expect(getByTestId('count-value').innerHTML).toBe('1'),
      )
      await waitFor(() =>
        expect(getByTestId('count2-value').innerHTML).toBe('1'),
      )
    })

    it('should use default value if not in storage', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      expect(getByTestId('count3-value').innerHTML).toBe('0')
    })

    it('should handle non jsonable object in storage', async () => {
      storage.setItem(testKey, 'test string')

      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count-increase'))
      await waitFor(() =>
        expect(getByTestId('count-value').innerHTML).toBe('1'),
      )

      expect(getStateValue()).toStrictEqual({
        [getAtomKey('count')]: 1,
      })
    })

    it('should handle non jsonable object in state', async () => {
      let mock = jest.spyOn(JSON, 'stringify').mockImplementation(() => {
        throw Error('mock error')
      })

      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('count-increase'))
      await waitFor(() =>
        expect(getByTestId('count-value').innerHTML).toBe('1'),
      )
      expect(mock).toHaveBeenCalledTimes(1)
      expect(console.error).toHaveBeenCalledTimes(1)
    })

    it('should handle non-existent atom name stored in storage', async () => {
      storage.setItem(
        testKey,
        JSON.stringify({
          notExist: 'test value',
        }),
      )

      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      await waitFor(() =>
        expect(getByTestId('count-value').innerHTML).toBe('0'),
      )
    })

    it.skip('should handle updating multiple atomes', async () => {
      const { getByTestId } = render(
        <RecoilRoot>
          <Demo />
        </RecoilRoot>,
      )

      fireEvent.click(getByTestId('update-multiple'))
      await waitFor(() =>
        expect(getByTestId('count-value').innerHTML).toBe('10'),
      )

      await waitFor(() =>
        expect(getByTestId('count2-value').innerHTML).toBe('10'),
      )

      expect(getStateValue()).toStrictEqual({
        [getAtomKey('count')]: 10,
        [getAtomKey('countFamily__"2"')]: 10,
      })
    })
  })
}
Example #7
Source File: ResultsTable.tsx    From nextclade with MIT License 3 votes vote down vote up
export function ResultsTable() {
  const { t } = useTranslation()

  const seqNamesImmediate = useRecoilValue(seqNamesFilteredAtom)
  const seqNames = useDeferredValue(seqNamesImmediate)

  const columnWidthsPx = useRecoilValue(resultsTableColumnWidthsPxAtom)
  const dynamicColumnWidthPx = useRecoilValue(resultsTableDynamicColumnWidthPxAtom)
  const cladeNodeAttrKeys = useRecoilValue(cladeNodeAttrKeysAtom)
  const cladeNodeAttrDescs = useRecoilValue(cladeNodeAttrDescsAtom)
  const isResultsFilterPanelCollapsed = useRecoilValue(isResultsFilterPanelCollapsedAtom)
  const viewedGene = useRecoilValue(viewedGeneAtom)

  const rowData: TableRowDatum[] = useMemo(() => {
    return seqNames.map((seqName) => ({
      seqName,
      viewedGene,
      columnWidthsPx,
      dynamicColumnWidthPx,
      cladeNodeAttrKeys,
    }))
  }, [cladeNodeAttrKeys, columnWidthsPx, dynamicColumnWidthPx, seqNames, viewedGene])

  // TODO: we could use a map (object) and refer to filters by name,
  // in order to reduce code duplication in the state, callbacks and components being rendered
  const sortByIndexAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.index, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByIndexDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.index, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByNameAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.seqName, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByNameDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.seqName, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByQcIssuesAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.qcIssues, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByQcIssuesDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.qcIssues, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByCladeAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.clade, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByCladeDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.clade, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalMutationsAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalMutations, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalMutationsDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalMutations, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalNonAcgtnAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalNonACGTNs, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalNonAcgtnDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalNonACGTNs, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalNsAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalMissing, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalNsDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalMissing, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalGapsAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalGaps, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalGapsDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalGaps, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalInsertionsAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalInsertions, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalInsertionsDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalInsertions, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalFrameShiftsAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalFrameShifts, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalFrameShiftsDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalFrameShifts, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalStopCodonsAsc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalStopCodons, direction: SortDirection.asc  }), undefined)  }, []) // prettier-ignore
  const sortByTotalStopCodonsDesc = useRecoilCallback(({set}) => () => { set(sortAnalysisResultsAtom({ category: SortCategory.totalStopCodons, direction: SortDirection.desc  }), undefined)  }, []) // prettier-ignore
  const sortByKey = useRecoilCallback(({ set }) => (key: string, direction: SortDirection) => () => {
    set(sortAnalysisResultsByKeyAtom({ key, direction  }), undefined)
  }, []) // prettier-ignore

  const dynamicColumns = useMemo(() => {
    return cladeNodeAttrDescs.map(({ name: attrKey, displayName, description }) => {
      const sortAsc = sortByKey(attrKey, SortDirection.asc)
      const sortDesc = sortByKey(attrKey, SortDirection.desc)
      return (
        <TableHeaderCell key={attrKey} basis={dynamicColumnWidthPx} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{displayName}</TableCellText>
            <ResultsControlsSort sortAsc={sortAsc} sortDesc={sortDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-clade" wide>
            <h5>{`Column: ${displayName}`}</h5>
            <p>{description}</p>
          </ButtonHelpStyled>
        </TableHeaderCell>
      )
    })
  }, [cladeNodeAttrDescs, dynamicColumnWidthPx, sortByKey])

  return (
    <Table rounded={isResultsFilterPanelCollapsed}>
      <TableHeaderRow>
        <TableHeaderCell first basis={columnWidthsPx.id} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('ID')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByIndexAsc} sortDesc={sortByIndexDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-seq-id">
            <HelpTipsColumnId />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.seqName} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('Sequence name')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByNameAsc} sortDesc={sortByNameDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-seq-name">
            <HelpTipsColumnSeqName />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.qc} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('QC')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByQcIssuesAsc} sortDesc={sortByQcIssuesDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-qc">
            <HelpTipsColumnQC />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.clade} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('Clade')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByCladeAsc} sortDesc={sortByCladeDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-clade" wide>
            <HelpTipsColumnClade />
          </ButtonHelpStyled>
        </TableHeaderCell>

        {dynamicColumns}

        <TableHeaderCell basis={columnWidthsPx.mut} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('Mut.')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByTotalMutationsAsc} sortDesc={sortByTotalMutationsDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-mut">
            <HelpTipsColumnMut />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.nonACGTN} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('non-ACGTN')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByTotalNonAcgtnAsc} sortDesc={sortByTotalNonAcgtnDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-nonacgtn">
            <HelpTipsColumnNonAcgtn />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.ns} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('Ns')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByTotalNsAsc} sortDesc={sortByTotalNsDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-missing">
            <HelpTipsColumnMissing />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.gaps} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('Gaps')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByTotalGapsAsc} sortDesc={sortByTotalGapsDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-gaps">
            <HelpTipsColumnGaps />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.insertions} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('Ins.')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByTotalInsertionsAsc} sortDesc={sortByTotalInsertionsDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-insertions">
            <HelpTipsColumnInsertions />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.frameShifts} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('FS')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByTotalFrameShiftsAsc} sortDesc={sortByTotalFrameShiftsDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-frame-shifts">
            <HelpTipsColumnFrameShifts />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.stopCodons} grow={0} shrink={0}>
          <TableHeaderCellContent>
            <TableCellText>{t('SC')}</TableCellText>
            <ResultsControlsSort sortAsc={sortByTotalStopCodonsAsc} sortDesc={sortByTotalStopCodonsDesc} />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-stop-codons">
            <HelpTipsColumnStopCodons />
          </ButtonHelpStyled>
        </TableHeaderCell>

        <TableHeaderCell basis={columnWidthsPx.sequenceView} grow={1} shrink={0}>
          <TableHeaderCellContent>
            <SequenceSelector />
          </TableHeaderCellContent>
          <ButtonHelpStyled identifier="btn-help-col-seq-view" tooltipWidth="600px">
            <HelpTipsColumnSeqView />
          </ButtonHelpStyled>
        </TableHeaderCell>
      </TableHeaderRow>

      <AutoSizer>
        {({ width, height }) => {
          return (
            <FixedSizeList
              overscanCount={10}
              style={LIST_STYLE}
              width={width}
              height={height - HEADER_ROW_HEIGHT}
              itemCount={rowData.length}
              itemSize={ROW_HEIGHT}
              itemData={rowData}
            >
              {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
              {/* @ts-ignore */}
              {ResultsTableRow}
            </FixedSizeList>
          )
        }}
      </AutoSizer>
    </Table>
  )
}