From 94f9ce6ae8f4bf99cbb08c95d828a42f89c55dbc Mon Sep 17 00:00:00 2001 From: Mgrdich <46796009+Mgrdich@users.noreply.github.com> Date: Wed, 17 Apr 2024 12:35:28 +0400 Subject: [PATCH] FE: Remove lodash lib (#301) --- frontend/.eslintrc.json | 4 +- frontend/package.json | 2 - frontend/pnpm-lock.yaml | 10 ----- frontend/src/components/App.tsx | 2 +- .../Actions/__tests__/Actions.spec.tsx | 25 +++++++---- frontend/src/components/Connect/New/New.tsx | 3 +- .../ConsumerGroups/Details/Details.tsx | 7 ++- .../Topics/Topic/Messages/Filters/utils.ts | 2 +- .../Topics/Topic/SendMessage/utils.ts | 5 ++- .../utils/__test__/updateSortingState.spec.ts | 2 +- .../lib/functions/__tests__/compact.spec.ts | 34 ++++++++++++++ .../lib/functions/__tests__/groupBy.spec.ts | 45 +++++++++++++++++++ frontend/src/lib/functions/compact.ts | 13 ++++++ frontend/src/lib/functions/groupBy.ts | 22 +++++++++ frontend/src/lib/functions/keyBy.ts | 1 - frontend/src/lib/hooks/api/kafkaConnect.ts | 27 +++++++++-- .../src/widgets/ClusterConfigForm/schema.ts | 3 +- .../ClusterConfigForm/utils/getJaasConfig.ts | 4 +- 18 files changed, 173 insertions(+), 38 deletions(-) create mode 100644 frontend/src/lib/functions/__tests__/compact.spec.ts create mode 100644 frontend/src/lib/functions/__tests__/groupBy.spec.ts create mode 100644 frontend/src/lib/functions/compact.ts create mode 100644 frontend/src/lib/functions/groupBy.ts diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index ae5968839..a2f7fd806 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -75,7 +75,9 @@ { "props": true, "ignorePropertyModificationsFor": [ - "state" + "state", + "acc", + "accumulator" ] } ], diff --git a/frontend/package.json b/frontend/package.json index 61cb4e7be..cb0ce7690 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,6 @@ "classnames": "^2.2.6", "json-schema-faker": "^0.5.6", "jsonpath-plus": "^7.2.0", - "lodash": "^4.17.21", "lossless-json": "^2.0.8", "pretty-ms": "7.0.1", "react": "^18.1.0", @@ -62,7 +61,6 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/user-event": "^14.4.3", "@types/eventsource": "^1.1.8", - "@types/lodash": "^4.14.172", "@types/lossless-json": "^1.0.1", "@types/node": "^20.11.17", "@types/react": "^18.0.9", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3bd4445b1..b074cb11e 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -58,9 +58,6 @@ dependencies: jsonpath-plus: specifier: ^7.2.0 version: 7.2.0 - lodash: - specifier: ^4.17.21 - version: 4.17.21 lossless-json: specifier: ^2.0.8 version: 2.0.11 @@ -141,9 +138,6 @@ devDependencies: '@types/eventsource': specifier: ^1.1.8 version: 1.1.11 - '@types/lodash': - specifier: ^4.14.172 - version: 4.14.177 '@types/lossless-json': specifier: ^1.0.1 version: 1.0.2 @@ -1896,10 +1890,6 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/lodash@4.14.177: - resolution: {integrity: sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==} - dev: true - /@types/lossless-json@1.0.2: resolution: {integrity: sha512-RdV8M8qlWUpmk7gnY3fBB4TNn3Ab8hMMqhJC/sG77t8Zk+hjVwvZGTFv+upEBUkxXbq0+UxGAPhOml83w1IkIQ==} dev: true diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index a2e1b1c72..16dd1305d 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -97,7 +97,7 @@ const App: React.FC = () => { - + ); }; diff --git a/frontend/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx b/frontend/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx index 9dce7507f..27743949b 100644 --- a/frontend/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx +++ b/frontend/src/components/Connect/Details/Actions/__tests__/Actions.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorPath } from 'lib/paths'; import Actions from 'components/Connect/Details/Actions/Actions'; -import { ConnectorAction, ConnectorState } from 'generated-sources'; +import { Connector, ConnectorAction, ConnectorState } from 'generated-sources'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { @@ -10,7 +10,16 @@ import { useUpdateConnectorState, } from 'lib/hooks/api/kafkaConnect'; import { connector } from 'lib/fixtures/kafkaConnect'; -import set from 'lodash/set'; + +function setConnectorStatus(con: Connector, state: ConnectorState) { + return { + ...con, + status: { + ...con, + state, + }, + }; +} const mockHistoryPush = jest.fn(); const deleteConnector = jest.fn(); @@ -66,7 +75,7 @@ describe('Actions', () => { it('renders buttons when paused', async () => { (useConnector as jest.Mock).mockImplementation(() => ({ - data: set({ ...connector }, 'status.state', ConnectorState.PAUSED), + data: setConnectorStatus(connector, ConnectorState.PAUSED), })); renderComponent(); await afterClickRestartButton(); @@ -78,7 +87,7 @@ describe('Actions', () => { it('renders buttons when failed', async () => { (useConnector as jest.Mock).mockImplementation(() => ({ - data: set({ ...connector }, 'status.state', ConnectorState.FAILED), + data: setConnectorStatus(connector, ConnectorState.FAILED), })); renderComponent(); await afterClickRestartButton(); @@ -90,7 +99,7 @@ describe('Actions', () => { it('renders buttons when unassigned', async () => { (useConnector as jest.Mock).mockImplementation(() => ({ - data: set({ ...connector }, 'status.state', ConnectorState.UNASSIGNED), + data: setConnectorStatus(connector, ConnectorState.UNASSIGNED), })); renderComponent(); await afterClickRestartButton(); @@ -102,7 +111,7 @@ describe('Actions', () => { it('renders buttons when running connector action', async () => { (useConnector as jest.Mock).mockImplementation(() => ({ - data: set({ ...connector }, 'status.state', ConnectorState.RUNNING), + data: setConnectorStatus(connector, ConnectorState.RUNNING), })); renderComponent(); await afterClickRestartButton(); @@ -115,7 +124,7 @@ describe('Actions', () => { describe('mutations', () => { beforeEach(() => { (useConnector as jest.Mock).mockImplementation(() => ({ - data: set({ ...connector }, 'status.state', ConnectorState.RUNNING), + data: setConnectorStatus(connector, ConnectorState.RUNNING), })); }); @@ -185,7 +194,7 @@ describe('Actions', () => { it('calls resumeConnector when resume button clicked', async () => { const resumeConnector = jest.fn(); (useConnector as jest.Mock).mockImplementation(() => ({ - data: set({ ...connector }, 'status.state', ConnectorState.PAUSED), + data: setConnectorStatus(connector, ConnectorState.PAUSED), })); (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ mutateAsync: resumeConnector, diff --git a/frontend/src/components/Connect/New/New.tsx b/frontend/src/components/Connect/New/New.tsx index bf285838d..db1d82e3c 100644 --- a/frontend/src/components/Connect/New/New.tsx +++ b/frontend/src/components/Connect/New/New.tsx @@ -18,7 +18,6 @@ import { Button } from 'components/common/Button/Button'; import PageHeading from 'components/common/PageHeading/PageHeading'; import Heading from 'components/common/heading/Heading.styled'; import { useConnects, useCreateConnector } from 'lib/hooks/api/kafkaConnect'; -import get from 'lodash/get'; import { Connect } from 'generated-sources'; import * as S from './New.styled'; @@ -45,7 +44,7 @@ const New: React.FC = () => { mode: 'all', resolver: yupResolver(validationSchema), defaultValues: { - connectName: get(connects, '0.name', ''), + connectName: connects.length > 0 ? connects[0].name : '', name: '', config: '', }, diff --git a/frontend/src/components/ConsumerGroups/Details/Details.tsx b/frontend/src/components/ConsumerGroups/Details/Details.tsx index f5cae44df..e7f952c8b 100644 --- a/frontend/src/components/ConsumerGroups/Details/Details.tsx +++ b/frontend/src/components/ConsumerGroups/Details/Details.tsx @@ -11,7 +11,7 @@ import ClusterContext from 'components/contexts/ClusterContext'; import PageHeading from 'components/common/PageHeading/PageHeading'; import * as Metrics from 'components/common/Metrics'; import { Tag } from 'components/common/Tag/Tag.styled'; -import groupBy from 'lodash/groupBy'; +import groupBy from 'lib/functions/groupBy'; import { Table } from 'components/common/table/Table/Table.styled'; import getTagColor from 'components/common/Tag/getTagColor'; import { Dropdown } from 'components/common/Dropdown'; @@ -48,7 +48,10 @@ const Details: React.FC = () => { navigate(clusterConsumerGroupResetRelativePath); }; - const partitionsByTopic = groupBy(consumerGroup.data?.partitions, 'topic'); + const partitionsByTopic = groupBy( + consumerGroup.data?.partitions || [], + 'topic' + ); const filteredPartitionsByTopic = Object.keys(partitionsByTopic).filter( (el) => el.includes(searchValue) ); diff --git a/frontend/src/components/Topics/Topic/Messages/Filters/utils.ts b/frontend/src/components/Topics/Topic/Messages/Filters/utils.ts index 585541373..43efe5b20 100644 --- a/frontend/src/components/Topics/Topic/Messages/Filters/utils.ts +++ b/frontend/src/components/Topics/Topic/Messages/Filters/utils.ts @@ -1,6 +1,6 @@ import { Partition, PollingMode, SeekType } from 'generated-sources'; -import compact from 'lodash/compact'; import { Option } from 'react-multi-select-component'; +import compact from 'lib/functions/compact'; export function isModeOptionWithInput(value: PollingMode) { return ( diff --git a/frontend/src/components/Topics/Topic/SendMessage/utils.ts b/frontend/src/components/Topics/Topic/SendMessage/utils.ts index 46d9e1278..ddc3c8d68 100644 --- a/frontend/src/components/Topics/Topic/SendMessage/utils.ts +++ b/frontend/src/components/Topics/Topic/SendMessage/utils.ts @@ -6,7 +6,6 @@ import { import jsf from 'json-schema-faker'; import Ajv, { DefinedError } from 'ajv/dist/2020'; import addFormats from 'ajv-formats'; -import upperFirst from 'lodash/upperFirst'; jsf.option('fillProperties', false); jsf.option('alwaysFakeOptionals', true); @@ -53,6 +52,10 @@ export const getSerdeOptions = (items: SerdeDescription[]) => { }, []); }; +function upperFirst(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + export const validateBySchema = ( value: string, schema: string | undefined, diff --git a/frontend/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts b/frontend/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts index 07bd60991..4abcaa071 100644 --- a/frontend/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts +++ b/frontend/src/components/common/NewTable/utils/__test__/updateSortingState.spec.ts @@ -1,6 +1,6 @@ import updateSortingState from 'components/common/NewTable/utils/updateSortingState'; import { SortingState } from '@tanstack/react-table'; -import compact from 'lodash/compact'; +import compact from 'lib/functions/compact'; const updater = (previousState: SortingState): SortingState => { return compact( diff --git a/frontend/src/lib/functions/__tests__/compact.spec.ts b/frontend/src/lib/functions/__tests__/compact.spec.ts new file mode 100644 index 000000000..9871454e8 --- /dev/null +++ b/frontend/src/lib/functions/__tests__/compact.spec.ts @@ -0,0 +1,34 @@ +import compact from 'lib/functions/compact'; + +describe('compact', () => { + it('should remove falsey values from the array', () => { + const input = [0, 1, false, 2, '', 3, null, undefined, 4, NaN, 5]; + const expected = [1, 2, 3, 4, 5]; + expect(compact(input)).toEqual(expected); + }); + + it('should return an empty array if all values are falsey', () => { + const input = [0, false, '', null, undefined, NaN]; + const expected: number[] = []; + expect(compact(input)).toEqual(expected); + }); + + it('should return a new array with only truthy values preserved', () => { + const input = [1, 'hello', true, [], { a: 1 }, 42]; + const expected = [1, 'hello', true, [], { a: 1 }, 42]; + expect(compact(input)).toEqual(expected); + }); + + it('should preserve non-falsey values in their original order', () => { + const input = [1, null, 2, undefined, 3, false, 4]; + const expected = [1, 2, 3, 4]; + expect(compact(input)).toEqual(expected); + }); + + it('should not modify the original array', () => { + const input = [0, 1, 2, false, '', null, undefined, NaN]; + const inputCopy = [...input]; + compact(input); + expect(input).toEqual(inputCopy); + }); +}); diff --git a/frontend/src/lib/functions/__tests__/groupBy.spec.ts b/frontend/src/lib/functions/__tests__/groupBy.spec.ts new file mode 100644 index 000000000..4c2433453 --- /dev/null +++ b/frontend/src/lib/functions/__tests__/groupBy.spec.ts @@ -0,0 +1,45 @@ +import groupBy from 'lib/functions/groupBy'; + +describe('groupBy', () => { + it('should group objects in the array by the specified key', () => { + const input = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'Doe' }, + { id: 4, name: 'John' }, + ]; + + const result = groupBy(input, 'name'); + + expect(result).toEqual({ + John: [ + { id: 1, name: 'John' }, + { id: 4, name: 'John' }, + ], + Jane: [{ id: 2, name: 'Jane' }], + Doe: [{ id: 3, name: 'Doe' }], + }); + }); + + it('should return an empty object when the input array is empty', () => { + const result = groupBy([], 'name'); + + expect(result).toEqual({}); + }); + + it('should handle objects with undefined values for the specified key', () => { + const input = [ + { id: 1, name: 'John' }, + { id: 2, name: undefined }, + { id: 3, name: 'Doe' }, + { id: 4 }, + ]; + + const result = groupBy(input, 'name'); + + expect(result).toEqual({ + John: [{ id: 1, name: 'John' }], + Doe: [{ id: 3, name: 'Doe' }], + }); + }); +}); diff --git a/frontend/src/lib/functions/compact.ts b/frontend/src/lib/functions/compact.ts new file mode 100644 index 000000000..dad8315f2 --- /dev/null +++ b/frontend/src/lib/functions/compact.ts @@ -0,0 +1,13 @@ +/** + * @description + * Creates an array with all falsey values removed. The values false, null, 0, "", undefined, and NaN are + * falsey. + * + * @param array The array to compact. + * @return Returns the new array of filtered values. + */ +export default function compact( + array: Array +): T[] { + return array.filter(Boolean) as T[]; +} diff --git a/frontend/src/lib/functions/groupBy.ts b/frontend/src/lib/functions/groupBy.ts new file mode 100644 index 000000000..df2f5d149 --- /dev/null +++ b/frontend/src/lib/functions/groupBy.ts @@ -0,0 +1,22 @@ +export default function groupBy( + collections: T[], + key: string +) { + return collections.reduce>((acc, curr) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const groupByKey = key in curr ? curr[key] : null; + + if (typeof groupByKey !== 'string' && typeof groupByKey !== 'number') { + return acc; + } + + if (acc[groupByKey]) { + acc[groupByKey].push(curr); + return acc; + } + + acc[groupByKey] = [curr]; + return acc; + }, {}); +} diff --git a/frontend/src/lib/functions/keyBy.ts b/frontend/src/lib/functions/keyBy.ts index 8a5b84ed0..76a8f2f5a 100644 --- a/frontend/src/lib/functions/keyBy.ts +++ b/frontend/src/lib/functions/keyBy.ts @@ -13,7 +13,6 @@ export function keyBy>( return collection.reduce>((acc, cur) => { const key = cur[property] as unknown as PropertyKey; - // eslint-disable-next-line no-param-reassign acc[key] = cur; return acc; diff --git a/frontend/src/lib/hooks/api/kafkaConnect.ts b/frontend/src/lib/hooks/api/kafkaConnect.ts index c17b381e4..225e72165 100644 --- a/frontend/src/lib/hooks/api/kafkaConnect.ts +++ b/frontend/src/lib/hooks/api/kafkaConnect.ts @@ -5,7 +5,6 @@ import { NewConnector, } from 'generated-sources'; import { kafkaConnectApiClient as api } from 'lib/api'; -import sortBy from 'lodash/sortBy'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ClusterName } from 'lib/interfaces/cluster'; import { showSuccessAlert } from 'lib/errorHandling'; @@ -55,7 +54,16 @@ export function useConnectors(clusterName: ClusterName, search?: string) { connectorsKey(clusterName, search), () => api.getAllConnectors({ clusterName, search }), { - select: (data) => sortBy(data, 'name'), + select: (data) => + [...data].sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }), } ); } @@ -67,7 +75,20 @@ export function useConnectorTasks(props: UseConnectorProps) { connectorTasksKey(props), () => api.getConnectorTasks(props), { - select: (data) => sortBy(data, 'status.id'), + select: (data) => + [...data].sort((a, b) => { + const aid = a.status.id; + const bid = b.status.id; + + if (aid < bid) { + return -1; + } + + if (aid > bid) { + return 1; + } + return 0; + }), } ); } diff --git a/frontend/src/widgets/ClusterConfigForm/schema.ts b/frontend/src/widgets/ClusterConfigForm/schema.ts index abd17f34b..68ffaa743 100644 --- a/frontend/src/widgets/ClusterConfigForm/schema.ts +++ b/frontend/src/widgets/ClusterConfigForm/schema.ts @@ -1,4 +1,3 @@ -import { isArray } from 'lodash'; import { object, string, number, array, boolean, mixed, lazy } from 'yup'; const requiredString = string().required('required field'); @@ -61,7 +60,7 @@ const kafkaConnectSchema = object({ }); const kafkaConnectsSchema = lazy((value) => { - if (isArray(value)) { + if (Array.isArray(value)) { return array().of(kafkaConnectSchema); } return mixed().optional(); diff --git a/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts b/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts index 23578159d..006f401fa 100644 --- a/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts +++ b/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts @@ -1,5 +1,3 @@ -import { isUndefined } from 'lodash'; - const JAAS_CONFIGS = { 'SASL/GSSAPI': 'com.sun.security.auth.module.Krb5LoginModule', 'SASL/OAUTHBEARER': @@ -21,7 +19,7 @@ export const getJaasConfig = ( ) => { const optionsString = Object.entries(options) .map(([key, value]) => { - if (isUndefined(value)) return null; + if (value === undefined) return null; if (value === 'true' || value === 'false') { return ` ${key}=${value}`; }