diff --git a/package-lock.json b/package-lock.json
index 2319391ba6..69c38d7319 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15607,9 +15607,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
@@ -45218,9 +45218,9 @@
"dev": true
},
"follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true
},
"for-each": {
diff --git a/src/App.jsx b/src/App.jsx
index 67c92ee41d..ee9903e99c 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -8,6 +8,7 @@ import config from 'config'
import { SentryRoute } from 'sentry'
import BaseLayout from 'layouts/BaseLayout'
+import EnterpriseLoginLayout from 'layouts/EnterpriseLoginLayout'
import LoginLayout from 'layouts/LoginLayout'
import { useLocationParams } from 'services/navigation'
import { ToastNotificationProvider } from 'services/toastNotification'
@@ -143,9 +144,9 @@ const MainAppRoutes = () => (
{config.IS_SELF_HOSTED ? (
-
+
-
+
) : (
)}
diff --git a/src/layouts/EnterpriseLoginLayout/EnterpriseLoginLayout.spec.tsx b/src/layouts/EnterpriseLoginLayout/EnterpriseLoginLayout.spec.tsx
new file mode 100644
index 0000000000..2081809abe
--- /dev/null
+++ b/src/layouts/EnterpriseLoginLayout/EnterpriseLoginLayout.spec.tsx
@@ -0,0 +1,82 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter, Route } from 'react-router-dom'
+
+import EnterpriseLoginLayout from './EnterpriseLoginLayout'
+
+jest.mock('./Header', () => () => 'Header')
+jest.mock('layouts/Footer', () => () => 'Footer')
+jest.mock('shared/GlobalBanners', () => () => 'GlobalBanners')
+jest.mock('layouts/ToastNotifications', () => () => 'ToastNotifications')
+jest.mock('ui/SessionExpiryTracker', () => () => 'SessionExpiryTracker')
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+})
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ {children}
+
+
+
+)
+
+describe('EnterpriseLoginLayout', () => {
+ it('renders children', () => {
+ render(<>children>, { wrapper })
+
+ const children = screen.getByText(/children/)
+ expect(children).toBeInTheDocument()
+ })
+
+ it('renders global banners', () => {
+ render(<>children>, { wrapper })
+
+ const globalBanners = screen.getByText(/GlobalBanners/)
+ expect(globalBanners).toBeInTheDocument()
+ })
+
+ it('renders the header', () => {
+ render(<>children>, { wrapper })
+
+ const header = screen.getByText(/Header/)
+ expect(header).toBeInTheDocument()
+ })
+
+ it('renders the footer', () => {
+ render(<>children>, { wrapper })
+
+ const footer = screen.getByText(/Footer/)
+ expect(footer).toBeInTheDocument()
+ })
+
+ it('renders toast notifications', () => {
+ render(<>children>, { wrapper })
+
+ const ToastNotifications = screen.getByText(/ToastNotifications/)
+ expect(ToastNotifications).toBeInTheDocument()
+ })
+
+ it('renders the session expiry tracker if no tracking session found', () => {
+ render(<>children>, { wrapper })
+
+ const session = screen.getByText(/SessionExpiryTracker/)
+ expect(session).toBeInTheDocument()
+ })
+
+ it('does not render the session expiry tracker if tracking session found', () => {
+ localStorage.setItem('tracking-session-expiry', 'true')
+
+ render(<>children>, { wrapper })
+
+ const session = screen.queryByText(/SessionExpiryTracker/)
+ expect(session).not.toBeInTheDocument()
+ })
+})
diff --git a/src/layouts/EnterpriseLoginLayout/EnterpriseLoginLayout.tsx b/src/layouts/EnterpriseLoginLayout/EnterpriseLoginLayout.tsx
new file mode 100644
index 0000000000..50d921e320
--- /dev/null
+++ b/src/layouts/EnterpriseLoginLayout/EnterpriseLoginLayout.tsx
@@ -0,0 +1,45 @@
+import { Suspense } from 'react'
+
+import Footer from 'layouts/Footer'
+import ErrorBoundary from 'layouts/shared/ErrorBoundary'
+import NetworkErrorBoundary from 'layouts/shared/NetworkErrorBoundary'
+import ToastNotifications from 'layouts/ToastNotifications'
+import GlobalBanners from 'shared/GlobalBanners'
+import LoadingLogo from 'ui/LoadingLogo'
+import SessionExpiryTracker from 'ui/SessionExpiryTracker'
+
+import Header from './Header'
+
+const LOCAL_STORAGE_SESSION_TRACKING_KEY = 'tracking-session-expiry'
+
+const FullPageLoader = () => (
+
+
+
+)
+
+function EnterpriseLoginLayout({ children }: { children: React.ReactNode }) {
+ const isTrackingSession = localStorage.getItem(
+ LOCAL_STORAGE_SESSION_TRACKING_KEY
+ )
+ return (
+ <>
+
+ {!isTrackingSession && }
+ }>
+
+
+
+
+ {children}
+
+
+
+
+
+
+ >
+ )
+}
+
+export default EnterpriseLoginLayout
diff --git a/src/layouts/EnterpriseLoginLayout/Header/Header.spec.tsx b/src/layouts/EnterpriseLoginLayout/Header/Header.spec.tsx
new file mode 100644
index 0000000000..9e09abc703
--- /dev/null
+++ b/src/layouts/EnterpriseLoginLayout/Header/Header.spec.tsx
@@ -0,0 +1,42 @@
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter, Route } from 'react-router-dom'
+
+import Header from './Header'
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+)
+
+describe('Header', () => {
+ it('renders the header', () => {
+ render(, { wrapper })
+
+ const link = screen.getByText(/Link to Homepage/)
+ expect(link).toBeInTheDocument()
+ })
+
+ it('renders the docs link', () => {
+ render(, { wrapper })
+
+ const link = screen.getByText(/Docs/)
+ expect(link).toBeInTheDocument()
+ })
+
+ it('renders the support link', () => {
+ render(, { wrapper })
+
+ const link = screen.getByText(/Support/)
+ expect(link).toBeInTheDocument()
+ })
+
+ it('renders the blog link', () => {
+ render(, { wrapper })
+
+ const link = screen.getByText(/Blog/)
+ expect(link).toBeInTheDocument()
+ })
+})
diff --git a/src/layouts/EnterpriseLoginLayout/Header/Header.tsx b/src/layouts/EnterpriseLoginLayout/Header/Header.tsx
new file mode 100644
index 0000000000..5810faf661
--- /dev/null
+++ b/src/layouts/EnterpriseLoginLayout/Header/Header.tsx
@@ -0,0 +1,65 @@
+import cs from 'classnames'
+
+import { ReactComponent as CodecovIcon } from 'assets/svg/codecov.svg'
+import { useImpersonate } from 'services/impersonate'
+import A from 'ui/A'
+
+function Header() {
+ const { isImpersonating } = useImpersonate()
+
+ return (
+
+ )
+}
+
+export default Header
diff --git a/src/layouts/EnterpriseLoginLayout/Header/index.ts b/src/layouts/EnterpriseLoginLayout/Header/index.ts
new file mode 100644
index 0000000000..6f8c57905f
--- /dev/null
+++ b/src/layouts/EnterpriseLoginLayout/Header/index.ts
@@ -0,0 +1 @@
+export { default } from './Header'
diff --git a/src/layouts/EnterpriseLoginLayout/index.ts b/src/layouts/EnterpriseLoginLayout/index.ts
new file mode 100644
index 0000000000..8fd918b9b7
--- /dev/null
+++ b/src/layouts/EnterpriseLoginLayout/index.ts
@@ -0,0 +1 @@
+export { default } from './EnterpriseLoginLayout'
diff --git a/src/pages/PullRequestPage/Header/HeaderDefault/HeaderDefault.jsx b/src/pages/PullRequestPage/Header/HeaderDefault/HeaderDefault.jsx
index 258946a813..18c9d51460 100644
--- a/src/pages/PullRequestPage/Header/HeaderDefault/HeaderDefault.jsx
+++ b/src/pages/PullRequestPage/Header/HeaderDefault/HeaderDefault.jsx
@@ -11,7 +11,6 @@ import Icon from 'ui/Icon'
import { usePullHeadData } from './hooks'
import { pullStateToColor } from '../constants'
-import PendoLink from '../PendoLink'
function HeaderDefault() {
const { provider, owner, repo, pullId } = useParams()
@@ -59,7 +58,6 @@ function HeaderDefault() {
-
)
}
diff --git a/src/pages/PullRequestPage/Header/HeaderTeam/HeaderTeam.jsx b/src/pages/PullRequestPage/Header/HeaderTeam/HeaderTeam.jsx
index 72e36f63d4..9ecaea131c 100644
--- a/src/pages/PullRequestPage/Header/HeaderTeam/HeaderTeam.jsx
+++ b/src/pages/PullRequestPage/Header/HeaderTeam/HeaderTeam.jsx
@@ -12,7 +12,6 @@ import TotalsNumber from 'ui/TotalsNumber'
import { usePullHeadDataTeam } from './hooks'
import { pullStateToColor } from '../constants'
-import PendoLink from '../PendoLink'
function HeaderTeam() {
const { provider, owner, repo, pullId } = useParams()
@@ -72,7 +71,6 @@ function HeaderTeam() {
/>
-
)
}
diff --git a/src/pages/PullRequestPage/Header/PendoLink/PendoLink.spec.tsx b/src/pages/PullRequestPage/Header/PendoLink/PendoLink.spec.tsx
deleted file mode 100644
index f91e159f02..0000000000
--- a/src/pages/PullRequestPage/Header/PendoLink/PendoLink.spec.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { render, screen } from '@testing-library/react'
-
-import PendoLink from './PendoLink'
-
-jest.mock('shared/featureFlags')
-
-describe('PendoLink', () => {
- it('displays link', () => {
- render( )
-
- const link = screen.getByText('Does this report look accurate?')
- expect(link).toBeInTheDocument()
- })
-})
diff --git a/src/pages/PullRequestPage/Header/PendoLink/PendoLink.tsx b/src/pages/PullRequestPage/Header/PendoLink/PendoLink.tsx
deleted file mode 100644
index 03d5f33c4f..0000000000
--- a/src/pages/PullRequestPage/Header/PendoLink/PendoLink.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import A from 'ui/A'
-
-const PendoLink: React.FC = () => {
- return (
-
- {/* Need this rule here as the href will be handled by pendo */}
- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
- {/* This ts expect error is required because of weird types on the A */}
- {/* @ts-expect-error */}
-
- Does this report look accurate?
-
-
- )
-}
-
-export default PendoLink
diff --git a/src/pages/PullRequestPage/Header/PendoLink/index.js b/src/pages/PullRequestPage/Header/PendoLink/index.js
deleted file mode 100644
index ad5ec74121..0000000000
--- a/src/pages/PullRequestPage/Header/PendoLink/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './PendoLink'
diff --git a/src/pages/RepoPage/ComponentsTab/ComponentsTab.jsx b/src/pages/RepoPage/ComponentsTab/ComponentsTab.jsx
index f8c4ef9f16..f7122e279f 100644
--- a/src/pages/RepoPage/ComponentsTab/ComponentsTab.jsx
+++ b/src/pages/RepoPage/ComponentsTab/ComponentsTab.jsx
@@ -3,9 +3,7 @@ import { Redirect, useParams } from 'react-router-dom'
import { SentryRoute } from 'sentry'
import { useRepoSettingsTeam } from 'services/repo'
-import { useRepoFlagsSelect } from 'services/repo/useRepoFlagsSelect'
import { TierNames, useTier } from 'services/tier'
-import ComponentsNotConfigured from 'shared/ComponentsNotConfigured'
import blurredTable from './assets/blurredTable.png'
import BackfillBanners from './BackfillBanners/BackfillBanners'
@@ -24,12 +22,7 @@ const showComponentsTable = ({
return componentsMeasurementsActive && componentsMeasurementsBackfilled
}
-const showComponentsData = ({ componentsData }) => {
- return componentsData && componentsData?.length > 0
-}
-
function ComponentsTab() {
- const { data: componentsData } = useRepoFlagsSelect()
const { provider, owner, repo } = useParams()
const { data: tierData } = useTier({ owner, provider })
const { data: repoSettings } = useRepoSettingsTeam()
@@ -49,10 +42,6 @@ function ComponentsTab() {
return
}
- if (!showComponentsData({ componentsData })) {
- return
- }
-
return (
)
-const FlagTable = memo(function Table({
+export const FlagTable = memo(function Table({
tableData,
isLoading,
sorting,
setSorting,
}: {
- tableData: any[] // TODO: update type when we convert useRepoFlags to TS
+ tableData: ReturnType
isLoading: boolean
sorting?: SortingState
setSorting?: OnChangeFn | undefined
}) {
const table = useReactTable({
- columns,
data: tableData,
+ columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
+ manualSorting: true,
})
return (
@@ -176,12 +170,7 @@ const FlagTable = memo(function Table({
-
-
- ) : null}
+
+
+
))}
@@ -239,7 +226,7 @@ function LoadMoreTrigger({
return (
Loading
@@ -275,7 +262,7 @@ function FlagsTable() {
hasNextPage,
fetchNextPage,
isFetchingNextPage,
- } = useRepoFlagsTable(sorting[0]?.desc)
+ } = useRepoFlagsTable(sorting[0]?.desc ?? true)
useEffect(() => {
if (inView && hasNextPage) {
diff --git a/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/index.js b/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/index.ts
similarity index 100%
rename from src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/index.js
rename to src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/index.ts
diff --git a/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.spec.js b/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.spec.tsx
similarity index 58%
rename from src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.spec.js
rename to src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.spec.tsx
index 9f6cf5f39d..5b0e27f576 100644
--- a/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.spec.js
+++ b/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.spec.tsx
@@ -1,8 +1,8 @@
import { renderHook, waitFor } from '@testing-library/react'
-import { format, subDays, subMonths } from 'date-fns'
+import { format, sub, subDays, subMonths } from 'date-fns'
import { useParams } from 'react-router-dom'
-import { act } from 'react-test-renderer'
+import { TIME_OPTION_VALUES } from 'pages/RepoPage/shared/constants'
import { useLocationParams } from 'services/navigation'
import { useRepo } from 'services/repo'
import { useRepoFlags } from 'services/repo/useRepoFlags'
@@ -21,6 +21,11 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
}))
+const mockedUseParams = useParams as jest.Mock
+const mockedUseRepo = useRepo as jest.Mock
+const mockedUseRepoFlags = useRepoFlags as jest.Mock
+const mockedUseLocationParams = useLocationParams as jest.Mock
+
const flagsData = [
{
node: {
@@ -52,32 +57,62 @@ const emptyRepoFlagsMock = {
data: [],
}
+const defaultParams = {
+ search: '',
+ historicalTrend: TIME_OPTION_VALUES.LAST_3_MONTHS,
+ flags: [],
+}
+
+interface SetupArgs {
+ isEmptyRepoFlags?: boolean
+ noOldestCommit?: boolean
+ useParamsValue?: {
+ search?: string
+ historicalTrend?: string
+ flags?: string[]
+ }
+}
+
describe('useRepoFlagsTable', () => {
function setup({
- repoData,
- useParamsValue = { search: '', historicalTrend: '', flags: [] },
- }) {
- useParams.mockReturnValue({
+ isEmptyRepoFlags = false,
+ noOldestCommit = false,
+ useParamsValue = {},
+ }: SetupArgs) {
+ mockedUseParams.mockReturnValue({
provider: 'gh',
owner: 'codecov',
repo: 'gazebo',
})
- useRepo.mockReturnValue({
- data: {
- repository: { oldestCommitAt: '2020-06-11T18:28:52' },
- },
+ mockedUseLocationParams.mockReturnValue({
+ params: { ...defaultParams, ...useParamsValue },
})
- useRepoFlags.mockReturnValue(repoData)
- useLocationParams.mockReturnValue({
- params: useParamsValue,
- })
+ if (noOldestCommit) {
+ mockedUseRepo.mockReturnValue({
+ data: {
+ repository: { oldestCommitAt: null },
+ },
+ })
+ } else {
+ mockedUseRepo.mockReturnValue({
+ data: {
+ repository: { oldestCommitAt: '2020-06-11T18:28:52' },
+ },
+ })
+ }
+
+ if (isEmptyRepoFlags) {
+ mockedUseRepoFlags.mockReturnValue(emptyRepoFlagsMock)
+ } else {
+ mockedUseRepoFlags.mockReturnValue(repoFlagsMock)
+ }
}
it('returns data accordingly', () => {
- setup({ repoData: repoFlagsMock })
+ setup({})
- const { result } = renderHook(() => useRepoFlagsTable())
+ const { result } = renderHook(() => useRepoFlagsTable(false))
expect(result.current.data).toEqual(flagsData)
expect(result.current.isLoading).toEqual(false)
@@ -88,110 +123,119 @@ describe('useRepoFlagsTable', () => {
describe('when there is no data', () => {
it('returns an empty array', () => {
- setup({ repoData: emptyRepoFlagsMock })
- const { result } = renderHook(() => useRepoFlagsTable())
+ setup({ isEmptyRepoFlags: true })
+ const { result } = renderHook(() => useRepoFlagsTable(false))
expect(result.current.data).toEqual([])
})
})
- describe('when handleSort is triggered', () => {
+ describe('sorting', () => {
beforeEach(() => {
- setup({ repoData: emptyRepoFlagsMock })
+ setup({ isEmptyRepoFlags: true })
})
it('calls useRepoFlagsTable with desc value', async () => {
- const { result } = renderHook(() => useRepoFlagsTable(true))
-
- act(() => {
- result.current.handleSort([{ desc: true }])
- })
+ renderHook(() => useRepoFlagsTable(true))
await waitFor(() =>
expect(useRepoFlags).toHaveBeenCalledWith({
- afterDate: '2020-06-11',
+ afterDate: format(sub(new Date(), { months: 3 }), 'yyyy-MM-dd'),
beforeDate: format(new Date(), 'yyyy-MM-dd'),
filters: { term: '', flagsNames: [] },
- interval: 'INTERVAL_30_DAY',
+ interval: 'INTERVAL_7_DAY',
orderingDirection: 'DESC',
suspense: false,
})
)
})
- it('calls useRepoFlagsTable with asc value when the array is empty', async () => {
- const { result } = renderHook(() => useRepoFlagsTable())
-
- act(() => {
- result.current.handleSort([])
- })
+ it('calls useRepoFlagsTable with asc value', async () => {
+ renderHook(() => useRepoFlagsTable(false))
await waitFor(() =>
expect(useRepoFlags).toHaveBeenCalledWith({
- afterDate: '2020-06-11',
+ afterDate: format(sub(new Date(), { months: 3 }), 'yyyy-MM-dd'),
beforeDate: format(new Date(), 'yyyy-MM-dd'),
filters: { term: '', flagsNames: [] },
- interval: 'INTERVAL_30_DAY',
+ interval: 'INTERVAL_7_DAY',
orderingDirection: 'ASC',
suspense: false,
})
)
})
+ })
+
+ describe('when there is search param', () => {
+ it('calls useRepoFlagsTable with correct filters value', () => {
+ setup({ useParamsValue: { search: 'flag1' } })
- it('calls useRepoFlagsTable with asc value', async () => {
const { result } = renderHook(() => useRepoFlagsTable(false))
- act(() => {
- result.current.handleSort([{ desc: false }])
+ expect(result.current.isSearching).toEqual(true)
+ expect(useRepoFlags).toHaveBeenCalledWith({
+ afterDate: format(sub(new Date(), { months: 3 }), 'yyyy-MM-dd'),
+ beforeDate: format(new Date(), 'yyyy-MM-dd'),
+ filters: { term: 'flag1', flagsNames: [] },
+ interval: 'INTERVAL_7_DAY',
+ orderingDirection: 'ASC',
+ suspense: false,
})
+ })
+ })
+
+ describe('historical trend', () => {
+ describe('when historical trend param is empty', () => {
+ beforeEach(() => {
+ setup({})
+ })
+
+ it('calls useRepoFlagsTable with correct query params', () => {
+ renderHook(() => useRepoFlagsTable(false))
- await waitFor(() =>
expect(useRepoFlags).toHaveBeenCalledWith({
- afterDate: '2020-06-11',
+ afterDate: format(sub(new Date(), { months: 3 }), 'yyyy-MM-dd'),
beforeDate: format(new Date(), 'yyyy-MM-dd'),
filters: { term: '', flagsNames: [] },
- interval: 'INTERVAL_30_DAY',
+ interval: 'INTERVAL_7_DAY',
orderingDirection: 'ASC',
suspense: false,
})
- )
+ })
})
- })
- describe('when there is search param', () => {
- it('calls useRepoFlagsTable with correct filters value', () => {
- setup({
- repoData: repoFlagsMock,
- useParamsValue: { search: 'flag1' },
+ describe('when historical trend param is all time', () => {
+ beforeEach(() => {
+ setup({ useParamsValue: { historicalTrend: 'ALL_TIME' } })
})
- const { result } = renderHook(() => useRepoFlagsTable())
+ it('calls useRepoFlagsTable with correct query params', () => {
+ renderHook(() => useRepoFlagsTable(false))
- expect(result.current.isSearching).toEqual(true)
- expect(useRepoFlags).toHaveBeenCalledWith({
- afterDate: '2020-06-11',
- beforeDate: format(new Date(), 'yyyy-MM-dd'),
- filters: { term: 'flag1' },
- interval: 'INTERVAL_30_DAY',
- orderingDirection: 'ASC',
- suspense: false,
+ expect(useRepoFlags).toHaveBeenCalledWith({
+ afterDate: '2020-06-11',
+ beforeDate: format(new Date(), 'yyyy-MM-dd'),
+ filters: { term: '', flagsNames: [] },
+ interval: 'INTERVAL_30_DAY',
+ orderingDirection: 'ASC',
+ suspense: false,
+ })
})
})
- })
- describe('historical trend', () => {
- describe('when historical trend param is empty or all time is selected', () => {
+ describe('when historical trend param is all time, but we do not have date of oldest commit', () => {
beforeEach(() => {
setup({
- repoData: repoFlagsMock,
+ noOldestCommit: true,
+ useParamsValue: { historicalTrend: 'ALL_TIME' },
})
})
it('calls useRepoFlagsTable with correct query params', () => {
- renderHook(() => useRepoFlagsTable())
+ renderHook(() => useRepoFlagsTable(false))
expect(useRepoFlags).toHaveBeenCalledWith({
- afterDate: '2020-06-11',
+ afterDate: format(sub(new Date(), { months: 6 }), 'yyyy-MM-dd'),
beforeDate: format(new Date(), 'yyyy-MM-dd'),
filters: { term: '', flagsNames: [] },
interval: 'INTERVAL_30_DAY',
@@ -204,19 +248,18 @@ describe('useRepoFlagsTable', () => {
describe('when 6 months is selected', () => {
beforeEach(() => {
setup({
- repoData: repoFlagsMock,
useParamsValue: { historicalTrend: 'LAST_6_MONTHS', search: '' },
})
})
it('calls useRepoFlagsTable with correct query params', () => {
- renderHook(() => useRepoFlagsTable())
+ renderHook(() => useRepoFlagsTable(false))
const afterDate = format(subMonths(new Date(), 6), 'yyyy-MM-dd')
expect(useRepoFlags).toHaveBeenCalledWith({
afterDate,
beforeDate: format(new Date(), 'yyyy-MM-dd'),
- filters: { term: '' },
+ filters: { term: '', flagsNames: [] },
interval: 'INTERVAL_7_DAY',
orderingDirection: 'ASC',
suspense: false,
@@ -227,19 +270,18 @@ describe('useRepoFlagsTable', () => {
describe('when last 7 days is selected', () => {
beforeEach(() => {
setup({
- repoData: repoFlagsMock,
useParamsValue: { historicalTrend: 'LAST_7_DAYS', search: '' },
})
})
it('calls useRepoFlagsTable with correct query params', () => {
- renderHook(() => useRepoFlagsTable())
+ renderHook(() => useRepoFlagsTable(false))
const afterDate = format(subDays(new Date(), 7), 'yyyy-MM-dd')
expect(useRepoFlags).toHaveBeenCalledWith({
afterDate,
beforeDate: format(new Date(), 'yyyy-MM-dd'),
- filters: { term: '' },
+ filters: { term: '', flagsNames: [] },
interval: 'INTERVAL_1_DAY',
orderingDirection: 'ASC',
suspense: false,
@@ -251,17 +293,16 @@ describe('useRepoFlagsTable', () => {
describe('when there is a flags param', () => {
it('calls useRepoFlagsTable with correct filters values', () => {
setup({
- repoData: repoFlagsMock,
useParamsValue: { flags: ['flag1'] },
})
- renderHook(() => useRepoFlagsTable())
+ renderHook(() => useRepoFlagsTable(false))
expect(useRepoFlags).toHaveBeenCalledWith({
- afterDate: '2020-06-11',
+ afterDate: format(sub(new Date(), { months: 3 }), 'yyyy-MM-dd'),
beforeDate: format(new Date(), 'yyyy-MM-dd'),
- filters: { flagsNames: ['flag1'] },
- interval: 'INTERVAL_30_DAY',
+ filters: { term: '', flagsNames: ['flag1'] },
+ interval: 'INTERVAL_7_DAY',
orderingDirection: 'ASC',
suspense: false,
})
diff --git a/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.js b/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.ts
similarity index 58%
rename from src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.js
rename to src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.ts
index 4eb510d172..05c0983621 100644
--- a/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.js
+++ b/src/pages/RepoPage/FlagsTab/subroute/FlagsTable/hooks/useRepoFlagsTable.ts
@@ -1,21 +1,26 @@
import { format, sub } from 'date-fns'
-import { useCallback, useState } from 'react'
import { useParams } from 'react-router-dom'
-import { SortingDirection } from 'old_ui/Table/constants'
import {
AFTER_DATE_FORMAT_OPTIONS,
MEASUREMENT_TIME_INTERVALS,
+ TIME_OPTION_KEY,
+ TIME_OPTION_VALUES,
} from 'pages/RepoPage/shared/constants'
import { useLocationParams } from 'services/navigation'
import { useRepo } from 'services/repo'
import { useRepoFlags } from 'services/repo/useRepoFlags'
-const getSortByDirection = (isDesc) =>
- isDesc ? SortingDirection.DESC : SortingDirection.ASC
+const getSortByDirection = (isDesc: boolean) => (isDesc ? 'DESC' : 'ASC')
-const createMeasurementVariables = (historicalTrend, oldestCommitAt) => {
- const isAllTime = !Boolean(historicalTrend) || historicalTrend === 'ALL_TIME'
+const createMeasurementVariables = (
+ historicalTrend: TIME_OPTION_KEY,
+ oldestCommitAt: string = format(
+ sub(new Date(), { ...AFTER_DATE_FORMAT_OPTIONS.LAST_6_MONTHS }),
+ 'yyyy-MM-dd'
+ )
+) => {
+ const isAllTime = historicalTrend === TIME_OPTION_VALUES.ALL_TIME
const selectedDate = isAllTime
? new Date(oldestCommitAt)
: sub(new Date(), { ...AFTER_DATE_FORMAT_OPTIONS[historicalTrend] })
@@ -27,48 +32,48 @@ const createMeasurementVariables = (historicalTrend, oldestCommitAt) => {
return { afterDate, interval }
}
-function useRepoFlagsTable(isDesc) {
+type URLParams = {
+ provider: string
+ owner: string
+ repo: string
+}
+
+function useRepoFlagsTable(isDesc: boolean) {
const { params } = useLocationParams({
search: '',
- historicalTrend: '',
+ historicalTrend: TIME_OPTION_VALUES.LAST_3_MONTHS,
flags: [],
})
- const { provider, owner, repo } = useParams()
+ const { provider, owner, repo } = useParams()
const { data: repoData } = useRepo({
provider,
owner,
repo,
})
const isAdmin = repoData?.isAdmin
+ // @ts-expect-errors, useLocation params needs to be updated to have full types
const isSearching = Boolean(params?.search)
- const [sortBy, setSortBy] = useState(SortingDirection.ASC)
const { afterDate, interval } = createMeasurementVariables(
- params?.historicalTrend,
- repoData?.repository?.oldestCommitAt
+ // @ts-expect-errors, useLocation params needs to be updated to have full types
+ params?.historicalTrend ?? TIME_OPTION_VALUES.LAST_3_MONTHS,
+ repoData?.repository?.oldestCommitAt ?? undefined
)
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } =
useRepoFlags({
+ // @ts-expect-errors, useLocation params needs to be updated to have full types
filters: { term: params?.search, flagsNames: params?.flags },
- orderingDirection: sortBy,
+ orderingDirection: getSortByDirection(isDesc),
beforeDate: format(new Date(), 'yyyy-MM-dd'),
interval,
afterDate,
suspense: false,
})
- const handleSort = useCallback(() => {
- const tableSortByDirection = getSortByDirection(isDesc)
- if (sortBy !== tableSortByDirection) {
- setSortBy(tableSortByDirection)
- }
- }, [isDesc, sortBy])
-
return {
data,
isAdmin,
isLoading,
- handleSort,
isSearching,
hasNextPage,
fetchNextPage,
diff --git a/src/pages/RepoPage/shared/constants.ts b/src/pages/RepoPage/shared/constants.ts
index b283c8f973..7ce916450e 100644
--- a/src/pages/RepoPage/shared/constants.ts
+++ b/src/pages/RepoPage/shared/constants.ts
@@ -27,6 +27,8 @@ export const TIME_OPTION_VALUES = {
LAST_7_DAYS: 'LAST_7_DAYS',
} as const
+export type TIME_OPTION_KEY = keyof typeof TIME_OPTION_VALUES
+
export const TimeOptions = [
{ label: 'All time', value: TIME_OPTION_VALUES.ALL_TIME },
{ label: 'Last 6 months', value: TIME_OPTION_VALUES.LAST_6_MONTHS },
diff --git a/src/sentry.ts b/src/sentry.ts
index 16c957f14b..a4da269fd1 100644
--- a/src/sentry.ts
+++ b/src/sentry.ts
@@ -89,6 +89,7 @@ export const setupSentry = ({
buttonLabel: 'Give Feedback',
submitButtonLabel: 'Send Feedback',
nameLabel: 'Username',
+ isEmailRequired: true,
})
integrations.push(feedback)
}
diff --git a/src/services/deleteComponentMeasurements/index.ts b/src/services/deleteComponentMeasurements/index.ts
new file mode 100644
index 0000000000..5ccdf7a767
--- /dev/null
+++ b/src/services/deleteComponentMeasurements/index.ts
@@ -0,0 +1 @@
+export * from './useDeleteComponentMeasurements'
diff --git a/src/services/deleteComponentMeasurements/useDeleteComponentMeasurements.spec.tsx b/src/services/deleteComponentMeasurements/useDeleteComponentMeasurements.spec.tsx
new file mode 100644
index 0000000000..25235d45d8
--- /dev/null
+++ b/src/services/deleteComponentMeasurements/useDeleteComponentMeasurements.spec.tsx
@@ -0,0 +1,140 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { renderHook, waitFor } from '@testing-library/react'
+import { graphql } from 'msw'
+import { setupServer } from 'msw/node'
+import { MemoryRouter, Route } from 'react-router-dom'
+
+import { useAddNotification } from 'services/toastNotification'
+
+import { useDeleteComponentMeasurements } from './useDeleteComponentMeasurements'
+
+jest.mock('services/toastNotification')
+
+const server = setupServer()
+
+beforeAll(() => server.listen())
+afterEach(() => {
+ server.resetHandlers()
+ queryClient.clear()
+})
+afterAll(() => server.close())
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+})
+
+const ownerUsername = 'codecov'
+const repoName = 'gazebo'
+
+const wrapper: React.FC = ({ children }) => (
+
+
+ {children}
+
+
+)
+
+describe('useDeleteComponentMeasurements', () => {
+ function setup(data = {}, triggerError = false) {
+ const mutate = jest.fn()
+ server.use(
+ graphql.mutation('deleteComponentMeasurements', (req, res, ctx) => {
+ mutate(req.variables)
+ if (triggerError) {
+ return res(ctx.status(500), ctx.data(data))
+ } else {
+ return res(ctx.status(200), ctx.data(data))
+ }
+ })
+ )
+
+ const addNotification = jest.fn()
+
+ //@ts-ignore
+ useAddNotification.mockReturnValue(addNotification)
+
+ return { addNotification, mutate }
+ }
+
+ describe('when called without an error', () => {
+ describe('When mutation is a success', () => {
+ it('returns successful response', async () => {
+ const { mutate } = setup({
+ deleteComponentMeasurements: {
+ ownerUsername,
+ repoName,
+ componentId: 'component-123',
+ },
+ })
+ const { result } = renderHook(() => useDeleteComponentMeasurements(), {
+ wrapper,
+ })
+ result.current.mutate({ componentId: 'component-123' })
+
+ await waitFor(() => expect(result.current.isSuccess).toBeTruthy())
+ await waitFor(() =>
+ expect(mutate).toHaveBeenCalledWith({
+ input: {
+ componentId: 'component-123',
+ ownerUsername: 'codecov',
+ repoName: 'gazebo',
+ },
+ })
+ )
+ })
+ })
+ })
+
+ describe('when called with a validation error', () => {
+ describe('When mutation is a success w/ a validation error', () => {
+ it('adds an error notification', async () => {
+ const mockData = {
+ deleteComponentMeasurements: {
+ error: {
+ __typename: 'ValidationError',
+ },
+ },
+ }
+ const { addNotification } = setup(mockData)
+ const { result } = renderHook(() => useDeleteComponentMeasurements(), {
+ wrapper,
+ })
+ result.current.mutate({ componentId: 'random-component-123' })
+
+ await waitFor(() =>
+ expect(addNotification).toHaveBeenCalledWith({
+ type: 'error',
+ text: 'There was an error deleting your component measurements',
+ })
+ )
+ })
+ })
+
+ describe('When mutation is not successful', () => {
+ it('adds an error notification', async () => {
+ const mockData = {
+ deleteComponentMeasurements: {
+ error: {
+ __typename: 'ValidationError',
+ },
+ },
+ }
+ const triggerError = true
+ const { addNotification } = setup(mockData, triggerError)
+ const { result } = renderHook(() => useDeleteComponentMeasurements(), {
+ wrapper,
+ })
+ result.current.mutate({ componentId: 'random-component-123' })
+
+ await waitFor(() =>
+ expect(addNotification).toHaveBeenCalledWith({
+ type: 'error',
+ text: 'There was an error deleting your component measurements',
+ })
+ )
+ })
+ })
+ })
+})
diff --git a/src/services/deleteComponentMeasurements/useDeleteComponentMeasurements.ts b/src/services/deleteComponentMeasurements/useDeleteComponentMeasurements.ts
new file mode 100644
index 0000000000..01b09e5478
--- /dev/null
+++ b/src/services/deleteComponentMeasurements/useDeleteComponentMeasurements.ts
@@ -0,0 +1,64 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { useParams } from 'react-router-dom'
+
+import { useAddNotification } from 'services/toastNotification'
+import Api from 'shared/api'
+
+interface URLParams {
+ provider: string
+ owner: string
+ repo: string
+}
+
+interface StarTrialMutationArgs {
+ componentId: string
+}
+
+export function useDeleteComponentMeasurements() {
+ const { provider, owner, repo } = useParams()
+ const queryClient = useQueryClient()
+ const addToast = useAddNotification()
+
+ return useMutation({
+ mutationFn: ({ componentId }: StarTrialMutationArgs) => {
+ const query = `
+ mutation deleteComponentMeasurements(
+ $input: DeleteComponentMeasurementsInput!
+ ) {
+ deleteComponentMeasurements(input: $input) {
+ error {
+ __typename
+ }
+ }
+ }
+ `
+ const variables = {
+ input: { ownerUsername: owner, repoName: repo, componentId },
+ }
+ return Api.graphqlMutation({
+ provider,
+ query,
+ variables,
+ mutationPath: 'deleteComponentMeasurements',
+ })
+ },
+ onSuccess: ({ data }) => {
+ const error = data?.deleteComponentMeasurements?.error?.__typename
+ if (error) {
+ // TODO: adjust backend to provide a message so we can tailor the message here
+ addToast({
+ type: 'error',
+ text: 'There was an error deleting your component measurements',
+ })
+ } else {
+ queryClient.invalidateQueries(['RepoFlags'])
+ }
+ },
+ onError: (e) => {
+ addToast({
+ type: 'error',
+ text: 'There was an error deleting your component measurements',
+ })
+ },
+ })
+}
diff --git a/src/services/repo/useRepoFlags.tsx b/src/services/repo/useRepoFlags.tsx
index 1f2a49b44c..3781bb04c6 100644
--- a/src/services/repo/useRepoFlags.tsx
+++ b/src/services/repo/useRepoFlags.tsx
@@ -77,7 +77,7 @@ const RepositorySchema = z.object({
}),
edges: z.array(
z.object({
- node: FlagEdgeSchema,
+ node: FlagEdgeSchema.nullable(),
})
),
}),
@@ -247,7 +247,7 @@ export function useRepoFlags({
})
return {
- data: data?.pages.map((page) => page?.flags).flat() ?? null,
+ data: data?.pages.map((page) => page?.flags).flat() ?? [],
...rest,
}
}
diff --git a/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.spec.tsx b/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.spec.tsx
index 0db2198022..57bc322afc 100644
--- a/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.spec.tsx
+++ b/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.spec.tsx
@@ -12,30 +12,20 @@ describe('ComponentsNotConfigured', () => {
)
describe('when rendered', () => {
- it('shows message', () => {
+ it('renders the no data message', () => {
render( , { wrapper })
- expect(
- screen.getByText(/The Components feature is not yet configured/)
- ).toBeInTheDocument()
- })
- it('renders link', () => {
- render( , { wrapper })
- const componentssAnchor = screen.getByRole('link', {
- name: /help your team today/i,
- })
- expect(componentssAnchor).toHaveAttribute(
- 'href',
- 'https://docs.codecov.com/docs/components'
- )
+ const noDataMessage = screen.getByText('No data to display')
+ expect(noDataMessage).toBeInTheDocument()
})
- it('renders empty state image', () => {
+ it('renders the configure components button', () => {
render( , { wrapper })
- const componentsMarketingImg = screen.getByRole('img', {
- name: /Components feature not configured/,
+
+ const configureComponentsButton = screen.getByRole('link', {
+ name: 'Get started with components external-link.svg',
})
- expect(componentsMarketingImg).toBeInTheDocument()
+ expect(configureComponentsButton).toBeInTheDocument()
})
})
})
diff --git a/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.tsx b/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.tsx
index 0dbb5e1e24..83411e7c18 100644
--- a/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.tsx
+++ b/src/shared/ComponentsNotConfigured/ComponentsNotConfigured.tsx
@@ -1,25 +1,24 @@
-import componentManagement from 'assets/flagManagement.svg'
-import A from 'ui/A'
+import Button from 'ui/Button'
function ComponentsNotConfigured() {
return (
-
-
-
-
- The Components feature is not yet configured{' '}
-
-
- Learn how components can{' '}
-
- help your team today
-
- .
-
+
+
+
No data to display
+
+ You will need to configure components in your yaml file to view the
+ list of your components here.
+
+
+
+
+ Get started with components
+
)