From 6e947a8ba66627826f3212257777f9abf4d92977 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:33:17 +0200 Subject: [PATCH] fix: linter rule for hooks (#9660) --- biome.json | 17 ++++- .../BillingPlan/BillingDetailsPAYG.tsx | 3 +- .../BillingPlan/BillingDetailsPro.tsx | 3 +- .../BillingPlan/useOverageCost.ts | 4 -- .../ChangeRequestTimeline.tsx | 18 +++-- .../RecentlyVisited/CommandResultGroup.tsx | 3 +- .../SearchHighlightContext.tsx | 3 +- .../FeatureOverviewCell.test.tsx | 8 +-- .../FeatureOverviewCell.tsx | 2 +- .../events/EventLog/EventLogFilters.test.tsx | 28 +++----- .../events/EventLog/EventLogFilters.tsx | 40 +++-------- .../EnvironmentAccordionBody.tsx | 8 +-- .../LegacyEnvironmentAccordionBody.tsx | 8 +-- .../ManageTagsDialog/ManageBulkTagsDialog.tsx | 2 +- .../ProjectFeatureToggles.tsx | 4 +- .../ManageTags.tsx | 1 - .../ProjectInsightsStats.tsx | 2 +- .../ChangeRequestTable.tsx | 69 ++++++++----------- 18 files changed, 100 insertions(+), 123 deletions(-) diff --git a/biome.json b/biome.json index 63273e98e9..c906123e9e 100644 --- a/biome.json +++ b/biome.json @@ -11,7 +11,8 @@ "noUnsafeOptionalChaining": "off", "useExhaustiveDependencies": "off", "noUnusedImports": "warn", - "useJsxKeyInIterable": "off" + "useJsxKeyInIterable": "off", + "useHookAtTopLevel": "error" }, "complexity": { "noBannedTypes": "off", @@ -100,5 +101,17 @@ "formatter": { "indentWidth": 2 } - } + }, + "overrides": [ + { + "include": ["src/**"], + "linter": { + "rules": { + "correctness": { + "useHookAtTopLevel": "off" + } + } + } + } + ] } diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx index 15687e8bc5..4a2316cb65 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx @@ -45,8 +45,7 @@ export const BillingDetailsPAYG = ({ const billableUsers = Math.max(eligibleUsers.length, minSeats); const usersCost = seatPrice * billableUsers; - const includedTraffic = BILLING_INCLUDED_REQUESTS; - const overageCost = useOverageCost(includedTraffic); + const overageCost = useOverageCost(BILLING_INCLUDED_REQUESTS); const totalCost = usersCost + overageCost; diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx index 9e5280d87f..9efef7293e 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPro.tsx @@ -52,8 +52,7 @@ export const BillingDetailsPro = ({ const freeAssigned = Math.min(eligibleUsers.length, seats); const paidAssigned = eligibleUsers.length - freeAssigned; const paidAssignedPrice = seatPrice * paidAssigned; - const includedTraffic = BILLING_INCLUDED_REQUESTS; - const overageCost = useOverageCost(includedTraffic); + const overageCost = useOverageCost(BILLING_INCLUDED_REQUESTS); const totalCost = planPrice + paidAssignedPrice + overageCost; diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/useOverageCost.ts b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/useOverageCost.ts index 1f29f95158..4a70803487 100644 --- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/useOverageCost.ts +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/useOverageCost.ts @@ -9,10 +9,6 @@ import { BILLING_TRAFFIC_PRICE } from './BillingPlan'; import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus'; export const useOverageCost = (includedTraffic: number) => { - if (!includedTraffic) { - return 0; - } - const now = new Date(); const formatDate = (date: Date) => format(date, 'yyyy-MM-dd'); const from = formatDate(startOfMonth(now)); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx index 130c70569f..9843c62750 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestTimeline/ChangeRequestTimeline.tsx @@ -12,7 +12,10 @@ import type { } from '../../changeRequest.types'; import { HtmlTooltip } from '../../../common/HtmlTooltip/HtmlTooltip'; import ErrorIcon from '@mui/icons-material/Error'; -import { useLocationSettings } from 'hooks/useLocationSettings'; +import { + type ILocationSettings, + useLocationSettings, +} from 'hooks/useLocationSettings'; import { formatDateYMDHMS } from 'utils/formatDate'; export type ISuggestChangeTimelineProps = @@ -99,6 +102,7 @@ export const ChangeRequestTimeline: FC = ({ data = steps; } const activeIndex = data.findIndex((item) => item === state); + const { locationSettings } = useLocationSettings(); return ( @@ -106,7 +110,10 @@ export const ChangeRequestTimeline: FC = ({ {data.map((title, index) => { if (schedule && title === 'Scheduled') { - return createTimelineScheduleItem(schedule); + return createTimelineScheduleItem( + schedule, + locationSettings, + ); } const color = determineColor( @@ -195,9 +202,10 @@ export const getScheduleProps = ( } }; -const createTimelineScheduleItem = (schedule: ChangeRequestSchedule) => { - const { locationSettings } = useLocationSettings(); - +const createTimelineScheduleItem = ( + schedule: ChangeRequestSchedule, + locationSettings: ILocationSettings, +) => { const time = formatDateYMDHMS( new Date(schedule.scheduledAt), locationSettings?.locale, diff --git a/frontend/src/component/commandBar/RecentlyVisited/CommandResultGroup.tsx b/frontend/src/component/commandBar/RecentlyVisited/CommandResultGroup.tsx index cc0d39e589..f23ace4d7d 100644 --- a/frontend/src/component/commandBar/RecentlyVisited/CommandResultGroup.tsx +++ b/frontend/src/component/commandBar/RecentlyVisited/CommandResultGroup.tsx @@ -169,9 +169,8 @@ export const RecentlyVisitedFeatureButton = ({ featureId: string; onClick: () => void; }) => { + const { trackEvent } = usePlausibleTracker(); const onItemClick = () => { - const { trackEvent } = usePlausibleTracker(); - trackEvent('command-bar', { props: { eventType: `click`, diff --git a/frontend/src/component/common/Table/SearchHighlightContext/SearchHighlightContext.tsx b/frontend/src/component/common/Table/SearchHighlightContext/SearchHighlightContext.tsx index 2a2e34a58c..5e74171ece 100644 --- a/frontend/src/component/common/Table/SearchHighlightContext/SearchHighlightContext.tsx +++ b/frontend/src/component/common/Table/SearchHighlightContext/SearchHighlightContext.tsx @@ -5,5 +5,6 @@ const SearchHighlightContext = createContext(''); export const SearchHighlightProvider = SearchHighlightContext.Provider; export const useSearchHighlightContext = (): { searchQuery: string } => { - return { searchQuery: useContext(SearchHighlightContext) }; + const searchQuery = useContext(SearchHighlightContext); + return { searchQuery }; }; diff --git a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.test.tsx b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.test.tsx index a8f43e72dc..39dcf5ffb9 100644 --- a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.test.tsx +++ b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.test.tsx @@ -1,11 +1,11 @@ import { screen } from '@testing-library/react'; import { render } from 'utils/testRenderer'; -import { FeatureOverviewCell as makeFeatureOverviewCell } from './FeatureOverviewCell'; +import { createFeatureOverviewCell } from './FeatureOverviewCell'; const noOp = () => {}; test('Display full overview information', () => { - const FeatureOverviewCell = makeFeatureOverviewCell(noOp, noOp); + const FeatureOverviewCell = createFeatureOverviewCell(noOp, noOp); render( { }); test('Display minimal overview information', () => { - const FeatureOverviewCell = makeFeatureOverviewCell(noOp, noOp); + const FeatureOverviewCell = createFeatureOverviewCell(noOp, noOp); render( { }); test('show archived information', () => { - const FeatureOverviewCell = makeFeatureOverviewCell(noOp, noOp); + const FeatureOverviewCell = createFeatureOverviewCell(noOp, noOp); render( void, onFlagTypeClick: (type: string) => void, diff --git a/frontend/src/component/events/EventLog/EventLogFilters.test.tsx b/frontend/src/component/events/EventLog/EventLogFilters.test.tsx index c6b7099acc..0c1a4cda27 100644 --- a/frontend/src/component/events/EventLog/EventLogFilters.test.tsx +++ b/frontend/src/component/events/EventLog/EventLogFilters.test.tsx @@ -6,12 +6,7 @@ const allFilterKeys = ['from', 'to', 'createdBy', 'type', 'project', 'feature']; allFilterKeys.sort(); test('When you have no projects or flags, you should not get a project or flag filters', () => { - const { result } = renderHook(() => - useEventLogFilters( - () => ({ projects: [] }), - () => ({ features: [] }), - ), - ); + const { result } = renderHook(() => useEventLogFilters([], [])); const filterKeys = result.current.map((filter) => filter.filterKey); filterKeys.sort(); @@ -22,9 +17,9 @@ test('When you have no projects or flags, you should not get a project or flag f test('When you have no projects, you should not get a project filter', () => { const { result } = renderHook(() => useEventLogFilters( - () => ({ projects: [] }), + [], // @ts-expect-error: omitting other properties we don't need - () => ({ features: [{ name: 'flag' }] }), + [{ name: 'flag' }], ), ); const filterKeys = result.current.map((filter) => filter.filterKey); @@ -35,10 +30,7 @@ test('When you have no projects, you should not get a project filter', () => { test('When you have only one project, you should not get a project filter', () => { const { result } = renderHook(() => - useEventLogFilters( - () => ({ projects: [{ id: 'a', name: 'A' }] }), - () => ({ features: [] }), - ), + useEventLogFilters([{ id: 'a', name: 'A' }], []), ); const filterKeys = result.current.map((filter) => filter.filterKey); filterKeys.sort(); @@ -49,13 +41,11 @@ test('When you have only one project, you should not get a project filter', () = test('When you have two one project, you should not get a project filter', () => { const { result } = renderHook(() => useEventLogFilters( - () => ({ - projects: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, - ], - }), - () => ({ features: [] }), + [ + { id: 'a', name: 'A' }, + { id: 'b', name: 'B' }, + ], + [], ), ); const filterKeys = result.current.map((filter) => filter.filterKey); diff --git a/frontend/src/component/events/EventLog/EventLogFilters.tsx b/frontend/src/component/events/EventLog/EventLogFilters.tsx index d25f8279a7..b126383bc1 100644 --- a/frontend/src/component/events/EventLog/EventLogFilters.tsx +++ b/frontend/src/component/events/EventLog/EventLogFilters.tsx @@ -6,24 +6,15 @@ import { } from 'component/filter/Filters/Filters'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; -import { - EventSchemaType, - type FeatureSearchResponseSchema, - type SearchFeaturesParams, -} from 'openapi'; +import { EventSchemaType, type FeatureSearchResponseSchema } from 'openapi'; import type { ProjectSchema } from 'openapi'; import { useEventCreators } from 'hooks/api/getters/useEventCreators/useEventCreators'; export const useEventLogFilters = ( - projectsHook: () => { projects: ProjectSchema[] }, - featuresHook: (params: SearchFeaturesParams) => { - features: FeatureSearchResponseSchema[]; - }, + projects: ProjectSchema[], + features: FeatureSearchResponseSchema[], ) => { - const { projects } = projectsHook(); - const { features } = featuresHook({}); const { eventCreators } = useEventCreators(); - const [availableFilters, setAvailableFilters] = useState([]); useEffect(() => { const projectOptions = @@ -124,22 +115,6 @@ export const useEventLogFilters = ( }; type LogType = 'flag' | 'project' | 'global'; -const useEventLogFiltersFromLogType = (logType: LogType) => { - switch (logType) { - case 'flag': - return useEventLogFilters( - () => ({ projects: [] }), - () => ({ features: [] }), - ); - case 'project': - return useEventLogFilters( - () => ({ projects: [] }), - useFeatureSearch, - ); - case 'global': - return useEventLogFilters(useProjects, useFeatureSearch); - } -}; type EventLogFiltersProps = { logType: LogType; @@ -154,7 +129,14 @@ export const EventLogFilters: FC = ({ state, onChange, }) => { - const availableFilters = useEventLogFiltersFromLogType(logType); + const { features } = useFeatureSearch({}); + const { projects } = useProjects(); + const featuresToFilter = logType !== 'flag' ? features : []; + const projectsToFilter = logType === 'global' ? projects : []; + const availableFilters = useEventLogFilters( + projectsToFilter, + featuresToFilter, + ); return ( (strategies, pageSize); + if (!featureEnvironment) { + return null; + } + const onReorder = async (payload: { id: string; sortOrder: number }[]) => { try { await setStrategiesSortOrder( diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/LegacyEnvironmentAccordionBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/LegacyEnvironmentAccordionBody.tsx index 7fbc33c681..98703fa5b3 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/LegacyEnvironmentAccordionBody.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/LegacyEnvironmentAccordionBody.tsx @@ -101,14 +101,14 @@ const EnvironmentAccordionBody = ({ } }, []); - if (!featureEnvironment) { - return null; - } - const pageSize = 20; const { page, pages, setPageIndex, pageIndex } = usePagination(strategies, pageSize); + if (!featureEnvironment) { + return null; + } + const onReorder = async (payload: { id: string; sortOrder: number }[]) => { try { await setStrategiesSortOrder( diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog.tsx index a878d919b4..2000a14aa9 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog.tsx @@ -11,7 +11,7 @@ import { TagTypeSelect } from './TagTypeSelect'; import { type TagOption, TagsInput } from './TagsInput'; import useTags from 'hooks/api/getters/useTags/useTags'; import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes'; -import type { ITag, ITagType } from 'interfaces/tags'; +import type { ITagType } from 'interfaces/tags'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; import type { TagSchema } from 'openapi'; diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx index 5860f02b5c..756dddd128 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -35,7 +35,7 @@ import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility'; import { TableEmptyState } from './TableEmptyState/TableEmptyState'; import { useRowActions } from './hooks/useRowActions'; import { useSelectedData } from './hooks/useSelectedData'; -import { FeatureOverviewCell } from 'component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell'; +import { createFeatureOverviewCell } from 'component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell'; import { useProjectFeatureSearch, useProjectFeatureSearchActions, @@ -213,7 +213,7 @@ export const ProjectFeatureToggles = ({ columnHelper.accessor('name', { id: 'name', header: 'Name', - cell: FeatureOverviewCell(onTagClick, onFlagTypeClick), + cell: createFeatureOverviewCell(onTagClick, onFlagTypeClick), enableHiding: false, }), columnHelper.accessor('createdAt', { diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx index 67c34a9fc1..5f4ca10008 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx @@ -2,7 +2,6 @@ import { useMemo, useState, type VFC } from 'react'; import { Button } from '@mui/material'; import { ManageBulkTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog'; import type { FeatureSchema, TagSchema } from 'openapi'; -import type { ITag } from 'interfaces/tags'; import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx index 62581cea8a..b687d6ff21 100644 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx +++ b/frontend/src/component/project/Project/ProjectInsights/ProjectInsightsStats/ProjectInsightsStats.tsx @@ -38,10 +38,10 @@ interface IProjectStatsProps { } export const ProjectInsightsStats = ({ stats }: IProjectStatsProps) => { + const projectId = useRequiredPathParam('projectId'); if (Object.keys(stats).length === 0) { return null; } - const projectId = useRequiredPathParam('projectId'); const { avgTimeToProdCurrentWindow, diff --git a/frontend/src/component/project/Project/ProjectSettings/ChangeRequestConfiguration/ChangeRequestTable.tsx b/frontend/src/component/project/Project/ProjectSettings/ChangeRequestConfiguration/ChangeRequestTable.tsx index 123444e5f7..edfac220ea 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ChangeRequestConfiguration/ChangeRequestTable.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ChangeRequestConfiguration/ChangeRequestTable.tsx @@ -1,4 +1,4 @@ -import { useContext, useMemo, useState, type VFC } from 'react'; +import { useContext, useMemo, useState, type VFC as FC } from 'react'; import { type HeaderGroup, useGlobalFilter, useTable } from 'react-table'; import { Alert, Box, styled, Typography } from '@mui/material'; import { @@ -41,8 +41,9 @@ const StyledBox = styled(Box)(({ theme }) => ({ }, })); -export const ChangeRequestTable: VFC = () => { +export const ChangeRequestTable: FC = () => { const { trackEvent } = usePlausibleTracker(); + const { hasAccess } = useContext(AccessContext); const [dialogState, setDialogState] = useState<{ isOpen: boolean; enableEnvironment: string; @@ -139,43 +140,33 @@ export const ChangeRequestTable: VFC = () => { }, { Header: 'Required approvals', - Cell: ({ row: { original } }: any) => { - const { hasAccess } = useContext(AccessContext); - - return ( - - { - onRequiredApprovalsChange( - original, - approvals, - ); - }} - disabled={ - !hasAccess( - [ - UPDATE_PROJECT, - PROJECT_CHANGE_REQUEST_WRITE, - ], - projectId, - ) - } - IconComponent={ - KeyboardArrowDownOutlined - } - fullWidth - /> - - } - /> - ); - }, + Cell: ({ row: { original } }: any) => + original.changeRequestEnabled ? ( + + { + onRequiredApprovalsChange( + original, + approvals, + ); + }} + disabled={ + !hasAccess( + [ + UPDATE_PROJECT, + PROJECT_CHANGE_REQUEST_WRITE, + ], + projectId, + ) + } + IconComponent={KeyboardArrowDownOutlined} + fullWidth + /> + + ) : null, width: 100, disableGlobalFilter: true, disableSortBy: true,