diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index 4be3239d4..10d5fd4cb 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -13,37 +13,45 @@ jobs: NODE_ENV: dev runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 with: # Disabling shallow clone is recommended for improving relevancy of reporting fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} token: ${{ github.token }} + - uses: pnpm/action-setup@v3.0.0 with: version: 8.6.12 + - name: Install node uses: actions/setup-node@v4.0.1 with: node-version: "18.17.1" cache: "pnpm" cache-dependency-path: "./frontend/pnpm-lock.yaml" + - name: Install Node dependencies run: | cd frontend/ pnpm install --frozen-lockfile - - name: Generate sources + + - name: Compile run: | cd frontend/ - pnpm gen:sources + pnpm compile + - name: Linter run: | cd frontend/ pnpm lint:CI + - name: Tests run: | cd frontend/ pnpm test:CI + - name: SonarCloud Scan if: false # TODO remove when public uses: sonarsource/sonarcloud-github-action@master diff --git a/.github/workflows/release-serde-api.yml b/.github/workflows/release-serde-api.yml index af5b1db0d..746769152 100644 --- a/.github/workflows/release-serde-api.yml +++ b/.github/workflows/release-serde-api.yml @@ -33,4 +33,8 @@ jobs: - name: Publish to Maven Central run: | - mvn source:jar javadoc:jar package gpg:sign -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Dserver.username=${{ secrets.NEXUS_USERNAME }} -Dserver.password=${{ secrets.NEXUS_PASSWORD }} nexus-staging:deploy -pl serde-api -s settings.xml + mvn source:jar javadoc:jar package gpg:sign \ + -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} \ + -Dserver.username=${{ secrets.NEXUS_USERNAME }} \ + -Dserver.password=${{ secrets.NEXUS_PASSWORD }} \ + central-publishing:publish -pl serde-api -s settings.xml diff --git a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java index 9c396e149..0a04ce9d6 100644 --- a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java +++ b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java @@ -17,6 +17,7 @@ import org.springframework.boot.info.GitProperties; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; @Service public class ApplicationInfoService { @@ -70,7 +71,7 @@ private List getEnabledFeatures() { // updating on startup and every hour @Scheduled(fixedRateString = "${github-release-info-update-rate:3600000}") public void updateGithubReleaseInfo() { - githubReleaseInfo.refresh().block(); + githubReleaseInfo.refresh().subscribe(); } } diff --git a/frontend/package.json b/frontend/package.json index 540c85a88..26be7c951 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,9 +50,9 @@ "scripts": { "start": "vite", "dev": "vite", - "clean": "rimraf ./src/generated-sources", - "gen:sources": "pnpm clean && openapi-generator-cli generate", - "build": "pnpm gen:sources && tsc --noEmit && vite build", + "compile": "pnpm gen:sources && tsc --noEmit", + "gen:sources": "rimraf ./src/generated-sources && openapi-generator-cli generate", + "build": "rimraf ./src/generated-sources && openapi-generator-cli generate && tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint --ext .tsx,.ts src/", "lint:fix": "eslint --ext .tsx,.ts src/ --fix", diff --git a/frontend/src/components/Brokers/Broker/Configs/Configs.styled.ts b/frontend/src/components/Brokers/Broker/Configs/Configs.styled.ts index cced53373..77768dc99 100644 --- a/frontend/src/components/Brokers/Broker/Configs/Configs.styled.ts +++ b/frontend/src/components/Brokers/Broker/Configs/Configs.styled.ts @@ -1,36 +1,6 @@ import styled from 'styled-components'; -export const ValueWrapper = styled.div` - display: flex; - justify-content: space-between; - button { - margin: 0 10px; - } -`; - -export const Value = styled.span` - line-height: 24px; - margin-right: 10px; - text-overflow: ellipsis; - max-width: 400px; - overflow: hidden; - white-space: nowrap; -`; - -export const ButtonsWrapper = styled.div` - display: flex; -`; export const SearchWrapper = styled.div` margin: 10px; width: 21%; `; - -export const Source = styled.div` - display: flex; - align-content: center; - svg { - margin-left: 10px; - vertical-align: middle; - cursor: pointer; - } -`; diff --git a/frontend/src/components/Brokers/Broker/Configs/Configs.tsx b/frontend/src/components/Brokers/Broker/Configs/Configs.tsx index 5b05b7cc5..54b9bd6a9 100644 --- a/frontend/src/components/Brokers/Broker/Configs/Configs.tsx +++ b/frontend/src/components/Brokers/Broker/Configs/Configs.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { CellContext, ColumnDef } from '@tanstack/react-table'; +import React, { type FC, useMemo, useState } from 'react'; import { ClusterBrokerParam } from 'lib/paths'; import useAppParams from 'lib/hooks/useAppParams'; import { @@ -7,91 +6,37 @@ import { useUpdateBrokerConfigByName, } from 'lib/hooks/api/brokers'; import Table from 'components/common/NewTable'; -import { BrokerConfig, ConfigSource } from 'generated-sources'; +import type { BrokerConfig } from 'generated-sources'; import Search from 'components/common/Search/Search'; -import Tooltip from 'components/common/Tooltip/Tooltip'; -import InfoIcon from 'components/common/Icons/InfoIcon'; +import { + getBrokerConfigsTableColumns, + getConfigTableData, +} from 'components/Brokers/Broker/Configs/lib/utils'; -import InputCell from './InputCell'; import * as S from './Configs.styled'; -const tooltipContent = `DYNAMIC_TOPIC_CONFIG = dynamic topic config that is configured for a specific topic -DYNAMIC_BROKER_LOGGER_CONFIG = dynamic broker logger config that is configured for a specific broker -DYNAMIC_BROKER_CONFIG = dynamic broker config that is configured for a specific broker -DYNAMIC_DEFAULT_BROKER_CONFIG = dynamic broker config that is configured as default for all brokers in the cluster -STATIC_BROKER_CONFIG = static broker config provided as broker properties at start up (e.g. server.properties file) -DEFAULT_CONFIG = built-in default configuration for configs that have a default value -UNKNOWN = source unknown e.g. in the ConfigEntry used for alter requests where source is not set`; - -const Configs: React.FC = () => { - const [keyword, setKeyword] = React.useState(''); +const Configs: FC = () => { + const [searchQuery, setSearchQuery] = useState(''); const { clusterName, brokerId } = useAppParams(); - const { data = [] } = useBrokerConfig(clusterName, Number(brokerId)); - const stateMutation = useUpdateBrokerConfigByName( + const { data: configs = [] } = useBrokerConfig(clusterName, Number(brokerId)); + const updateBrokerConfigByName = useUpdateBrokerConfigByName( clusterName, Number(brokerId) ); - const getData = () => { - return data - .filter((item) => { - const nameMatch = item.name - .toLocaleLowerCase() - .includes(keyword.toLocaleLowerCase()); - return nameMatch - ? true - : item.value && - item.value - .toLocaleLowerCase() - .includes(keyword.toLocaleLowerCase()); // try to match the keyword on any of the item.value elements when nameMatch fails but item.value exists - }) - .sort((a, b) => { - if (a.source === b.source) return 0; - return a.source === ConfigSource.DYNAMIC_BROKER_CONFIG ? -1 : 1; - }); - }; - - const dataSource = React.useMemo(() => getData(), [data, keyword]); - - const renderCell = (props: CellContext) => ( - { - stateMutation.mutateAsync({ - name, - brokerConfigItem: { - value, - }, - }); - }} - /> + const tableData = useMemo( + () => getConfigTableData(configs, searchQuery), + [configs, searchQuery] ); - const columns = React.useMemo[]>( - () => [ - { header: 'Key', accessorKey: 'name' }, - { - header: 'Value', - accessorKey: 'value', - cell: renderCell, - }, - { - // eslint-disable-next-line react/no-unstable-nested-components - header: () => { - return ( - - Source - } - content={tooltipContent} - placement="top-end" - /> - - ); - }, - accessorKey: 'source', - }, - ], + const onUpdateInputCell = async ( + name: BrokerConfig['name'], + value: BrokerConfig['value'] + ) => + updateBrokerConfigByName.mutateAsync({ name, brokerConfigItem: { value } }); + + const columns = useMemo( + () => getBrokerConfigsTableColumns(onUpdateInputCell), [] ); @@ -99,12 +44,12 @@ const Configs: React.FC = () => { <> - +
); }; diff --git a/frontend/src/components/Brokers/Broker/Configs/InputCell.tsx b/frontend/src/components/Brokers/Broker/Configs/InputCell.tsx deleted file mode 100644 index bf54c45c6..000000000 --- a/frontend/src/components/Brokers/Broker/Configs/InputCell.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useEffect } from 'react'; -import { CellContext } from '@tanstack/react-table'; -import CheckmarkIcon from 'components/common/Icons/CheckmarkIcon'; -import EditIcon from 'components/common/Icons/EditIcon'; -import CancelIcon from 'components/common/Icons/CancelIcon'; -import { useConfirm } from 'lib/hooks/useConfirm'; -import { Action, BrokerConfig, ResourceType } from 'generated-sources'; -import { Button } from 'components/common/Button/Button'; -import Input from 'components/common/Input/Input'; -import { ActionButton } from 'components/common/ActionComponent'; - -import * as S from './Configs.styled'; - -interface InputCellProps extends CellContext { - onUpdate: (name: string, value?: string) => void; -} - -const InputCell: React.FC = ({ row, getValue, onUpdate }) => { - const initialValue = `${getValue()}`; - const [isEdit, setIsEdit] = React.useState(false); - const [value, setValue] = React.useState(initialValue); - - const confirm = useConfirm(); - - const onSave = () => { - if (value !== initialValue) { - confirm('Are you sure you want to change the value?', async () => { - onUpdate(row?.original?.name, value); - }); - } - setIsEdit(false); - }; - - useEffect(() => { - setValue(initialValue); - }, [initialValue]); - - return isEdit ? ( - - setValue(target?.value)} - /> - - - - - - ) : ( - - {initialValue} - setIsEdit(true)} - permission={{ - resource: ResourceType.CLUSTERCONFIG, - action: Action.EDIT, - }} - > - Edit - - - ); -}; - -export default InputCell; diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/ConfigSourceHeader/ConfigSourceHeader.styled.ts b/frontend/src/components/Brokers/Broker/Configs/TableComponents/ConfigSourceHeader/ConfigSourceHeader.styled.ts new file mode 100644 index 000000000..3856932b1 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/ConfigSourceHeader/ConfigSourceHeader.styled.ts @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +export const Source = styled.div` + display: flex; + align-content: center; + + svg { + margin-left: 10px; + vertical-align: middle; + cursor: pointer; + } +`; diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/ConfigSourceHeader/ConfigSourceHeader.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/ConfigSourceHeader/ConfigSourceHeader.tsx new file mode 100644 index 000000000..e521218e8 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/ConfigSourceHeader/ConfigSourceHeader.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import InfoIcon from 'components/common/Icons/InfoIcon'; +import { CONFIG_SOURCE_NAME_MAP } from 'components/Brokers/Broker/Configs/lib/constants'; + +import * as S from './ConfigSourceHeader.styled'; + +const tooltipContent = `${CONFIG_SOURCE_NAME_MAP.DYNAMIC_TOPIC_CONFIG} = dynamic topic config that is configured for a specific topic +${CONFIG_SOURCE_NAME_MAP.DYNAMIC_BROKER_LOGGER_CONFIG} = dynamic broker logger config that is configured for a specific broker +${CONFIG_SOURCE_NAME_MAP.DYNAMIC_BROKER_CONFIG} = dynamic broker config that is configured for a specific broker +${CONFIG_SOURCE_NAME_MAP.DYNAMIC_DEFAULT_BROKER_CONFIG} = dynamic broker config that is configured as default for all brokers in the cluster +${CONFIG_SOURCE_NAME_MAP.STATIC_BROKER_CONFIG} = static broker config provided as broker properties at start up (e.g. server.properties file) +${CONFIG_SOURCE_NAME_MAP.DEFAULT_CONFIG} = built-in default configuration for configs that have a default value +${CONFIG_SOURCE_NAME_MAP.UNKNOWN} = source unknown e.g. in the ConfigEntry used for alter requests where source is not set`; + +const ConfigSourceHeader = () => ( + + Source + } + content={tooltipContent} + placement="top-end" + /> + +); + +export default ConfigSourceHeader; diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellEditMode.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellEditMode.tsx new file mode 100644 index 000000000..2ac00ca63 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellEditMode.tsx @@ -0,0 +1,53 @@ +import React, { type FC, useState } from 'react'; +import Input from 'components/common/Input/Input'; +import { Button } from 'components/common/Button/Button'; +import CheckmarkIcon from 'components/common/Icons/CheckmarkIcon'; +import CancelIcon from 'components/common/Icons/CancelIcon'; + +import * as S from './styled'; + +interface EditModeProps { + initialValue: string; + onSave: (value: string) => void; + onCancel: () => void; +} + +const InputCellEditMode: FC = ({ + initialValue, + onSave, + onCancel, +}) => { + const [value, setValue] = useState(initialValue); + + return ( + + setValue(target.value)} + /> + + + + + + ); +}; + +export default InputCellEditMode; diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode.tsx new file mode 100644 index 000000000..1ddfa2b03 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode.tsx @@ -0,0 +1,56 @@ +import React, { type FC } from 'react'; +import { Button } from 'components/common/Button/Button'; +import EditIcon from 'components/common/Icons/EditIcon'; +import type { ConfigUnit } from 'components/Brokers/Broker/Configs/lib/types'; +import Tooltip from 'components/common/Tooltip/Tooltip'; +import { getConfigDisplayValue } from 'components/Brokers/Broker/Configs/lib/utils'; + +import * as S from './styled'; + +interface InputCellViewModeProps { + value: string; + unit: ConfigUnit | undefined; + onEdit: () => void; + isDynamic: boolean; + isSensitive: boolean; + isReadOnly: boolean; +} + +const InputCellViewMode: FC = ({ + value, + unit, + onEdit, + isDynamic, + isSensitive, + isReadOnly, +}) => { + const { displayValue, title } = getConfigDisplayValue( + isSensitive, + value, + unit + ); + + return ( + + {displayValue} + + Edit + + } + showTooltip={isReadOnly} + content="Property is read-only" + placement="left" + /> + + ); +}; + +export default InputCellViewMode; diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCell.spec.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCell.spec.tsx new file mode 100644 index 000000000..421b00f21 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCell.spec.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import InputCell, { + type InputCellProps, +} from 'components/Brokers/Broker/Configs/TableComponents/InputCell/index'; +import { render } from 'lib/testHelpers'; +import { ConfigSource } from 'generated-sources'; +import { useConfirm } from 'lib/hooks/useConfirm'; +import { BrokerConfigsTableRow } from 'components/Brokers/Broker/Configs/lib/types'; +import { Row } from '@tanstack/react-table'; + +jest.mock('lib/hooks/useConfirm', () => ({ + useConfirm: jest.fn(), +})); + +describe('InputCell', () => { + const mockOnUpdate = jest.fn(); + const initialValue = 'initialValue'; + const name = 'testName'; + const original = { + name, + source: ConfigSource.DYNAMIC_BROKER_CONFIG, + value: initialValue, + isSensitive: false, + isReadOnly: false, + }; + + beforeEach(() => { + const setupWrapper = (props?: Partial) => ( + } + onUpdate={mockOnUpdate} + /> + ); + (useConfirm as jest.Mock).mockImplementation( + () => (message: string, callback: () => void) => callback() + ); + render(setupWrapper()); + }); + + it('renders InputCellViewMode by default', () => { + expect(screen.getByText(initialValue)).toBeInTheDocument(); + }); + + it('switches to InputCellEditMode upon triggering an edit action', async () => { + const user = userEvent.setup(); + await user.click(screen.getByLabelText('editAction')); + expect( + screen.getByRole('textbox', { name: /inputValue/i }) + ).toBeInTheDocument(); + }); + + it('calls onUpdate callback with the new value when saved', async () => { + const user = userEvent.setup(); + await user.click(screen.getByLabelText('editAction')); // Enter edit mode + await user.type( + screen.getByRole('textbox', { name: /inputValue/i }), + '123' + ); + await user.click(screen.getByRole('button', { name: /confirmAction/i })); + expect(mockOnUpdate).toHaveBeenCalledWith(name, 'initialValue123'); + }); + + it('returns to InputCellViewMode upon canceling an edit', async () => { + const user = userEvent.setup(); + await user.click(screen.getByLabelText('editAction')); // Enter edit mode + await user.click(screen.getByRole('button', { name: /cancelAction/i })); + expect(screen.getByText(initialValue)).toBeInTheDocument(); // Back to view mode + }); +}); diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCellEditMode.spec.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCellEditMode.spec.tsx new file mode 100644 index 000000000..91270469e --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCellEditMode.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import InputCellEditMode from 'components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellEditMode'; +import { render } from 'lib/testHelpers'; +import userEvent from '@testing-library/user-event'; + +describe('InputCellEditMode', () => { + const mockOnSave = jest.fn(); + const mockOnCancel = jest.fn(); + + beforeEach(() => { + render( + + ); + }); + + it('renders with initial value', () => { + expect(screen.getByRole('textbox', { name: /inputValue/i })).toHaveValue( + 'test' + ); + }); + + it('calls onSave with new value', async () => { + const user = userEvent.setup(); + await user.type( + screen.getByRole('textbox', { name: /inputValue/i }), + '123' + ); + await user.click(screen.getByRole('button', { name: /confirmAction/i })); + expect(mockOnSave).toHaveBeenCalledWith('test123'); + }); + + it('calls onCancel', async () => { + const user = userEvent.setup(); + await user.click(screen.getByRole('button', { name: /cancelAction/i })); + expect(mockOnCancel).toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCellViewMode.spec.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCellViewMode.spec.tsx new file mode 100644 index 000000000..9499fa857 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/__test__/InputCellViewMode.spec.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from 'lib/testHelpers'; +import InputCellViewMode from 'components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode'; + +describe('InputCellViewMode', () => { + const mockOnEdit = jest.fn(); + const value = 'testValue'; + + it('displays the correct value for non-sensitive data', () => { + render( + + ); + expect(screen.getByTitle(value)).toBeInTheDocument(); + }); + + it('masks sensitive data with asterisks', () => { + render( + + ); + expect(screen.getByTitle('Sensitive Value')).toBeInTheDocument(); + expect(screen.getByText('**********')).toBeInTheDocument(); + }); + + it('renders edit button and triggers onEdit callback when clicked', async () => { + const user = userEvent.setup(); + render( + + ); + await user.click(screen.getByLabelText('editAction')); + expect(mockOnEdit).toHaveBeenCalled(); + }); + + it('disables edit button for read-only properties', () => { + render( + + ); + expect(screen.getByLabelText('editAction')).toBeDisabled(); + }); +}); diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/index.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/index.tsx new file mode 100644 index 000000000..a55d7852c --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/index.tsx @@ -0,0 +1,60 @@ +import React, { type FC, useState } from 'react'; +import { useConfirm } from 'lib/hooks/useConfirm'; +import { type CellContext } from '@tanstack/react-table'; +import { type BrokerConfig } from 'generated-sources'; +import { + BrokerConfigsTableRow, + UpdateBrokerConfigCallback, +} from 'components/Brokers/Broker/Configs/lib/types'; +import { getConfigUnit } from 'components/Brokers/Broker/Configs/lib/utils'; + +import InputCellViewMode from './InputCellViewMode'; +import InputCellEditMode from './InputCellEditMode'; + +export interface InputCellProps + extends CellContext { + onUpdate: UpdateBrokerConfigCallback; +} + +const InputCell: FC = ({ row, onUpdate }) => { + const [isEdit, setIsEdit] = useState(false); + const confirm = useConfirm(); + const { + name, + source, + value: initialValue, + isSensitive, + isReadOnly, + } = row.original; + + const handleSave = (newValue: string) => { + if (newValue !== initialValue) { + confirm('Are you sure you want to change the value?', () => + onUpdate(name, newValue) + ); + } + setIsEdit(false); + }; + + const isDynamic = source === 'DYNAMIC_BROKER_CONFIG'; + const configUnit = getConfigUnit(name); + + return isEdit ? ( + setIsEdit(false)} + /> + ) : ( + setIsEdit(true)} + isDynamic={isDynamic} + isSensitive={isSensitive} + isReadOnly={isReadOnly} + /> + ); +}; + +export default InputCell; diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/styled.ts b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/styled.ts new file mode 100644 index 000000000..859b2d5d1 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/styled.ts @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +export const ValueWrapper = styled.div<{ $isDynamic?: boolean }>` + display: flex; + justify-content: space-between; + font-weight: ${({ $isDynamic }) => ($isDynamic ? 600 : 400)}; + + button { + margin: 0 10px; + } +`; + +export const Value = styled.span` + line-height: 24px; + margin-right: 10px; + text-overflow: ellipsis; + max-width: 400px; + overflow: hidden; + white-space: nowrap; +`; + +export const ButtonsWrapper = styled.div` + display: flex; +`; diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/index.ts b/frontend/src/components/Brokers/Broker/Configs/TableComponents/index.ts new file mode 100644 index 000000000..308200222 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/index.ts @@ -0,0 +1,2 @@ +export { default as InputCell } from './InputCell'; +export { default as ConfigSourceHeader } from './ConfigSourceHeader/ConfigSourceHeader'; diff --git a/frontend/src/components/Brokers/Broker/Configs/lib/__test__/utils.spec.tsx b/frontend/src/components/Brokers/Broker/Configs/lib/__test__/utils.spec.tsx new file mode 100644 index 000000000..c809e6ebb --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/lib/__test__/utils.spec.tsx @@ -0,0 +1,88 @@ +import { + getConfigDisplayValue, + getConfigTableData, + getConfigUnit, +} from 'components/Brokers/Broker/Configs/lib/utils'; +import { ConfigSource } from 'generated-sources'; +import { render } from 'lib/testHelpers'; +import { ReactElement } from 'react'; + +describe('getConfigTableData', () => { + it('filters configs by search query and sorts by source priority', () => { + const configs = [ + { + name: 'log.retention.ms', + value: '7200000', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: true, + isReadOnly: false, + }, + { + name: 'log.segment.bytes', + value: '1073741824', + source: ConfigSource.DYNAMIC_BROKER_CONFIG, + isSensitive: false, + isReadOnly: true, + }, + { + name: 'compression.type', + value: 'producer', + source: ConfigSource.DEFAULT_CONFIG, + isSensitive: true, + isReadOnly: false, + }, + ]; + const searchQuery = 'log'; + const result = getConfigTableData(configs, searchQuery); + + expect(result).toHaveLength(2); + expect(result[0].name).toBe('log.segment.bytes'); + expect(result[1].name).toBe('log.retention.ms'); + }); +}); + +describe('getConfigUnit', () => { + it('identifies the unit of a configuration name', () => { + expect(getConfigUnit('log.retention.ms')).toBe('ms'); + expect(getConfigUnit('log.segment.bytes')).toBe('bytes'); + expect(getConfigUnit('compression.type')).toBeUndefined(); + }); +}); + +describe('getConfigDisplayValue', () => { + it('masks sensitive data with asterisks', () => { + const result = getConfigDisplayValue(true, 'testValue', undefined); + expect(result).toEqual({ + displayValue: '**********', + title: 'Sensitive Value', + }); + }); + + it('returns formatted bytes when unit is "bytes" and value is positive', () => { + const { container } = render( + getConfigDisplayValue(false, '1024', 'bytes').displayValue as ReactElement + ); + expect(container).toHaveTextContent('1 KB'); + expect(getConfigDisplayValue(false, '1024', 'bytes').title).toBe( + 'Bytes: 1024' + ); + }); + + it('returns value as is when unit is "bytes" but value is non-positive', () => { + const result = getConfigDisplayValue(false, '-1', 'bytes'); + expect(result.displayValue).toBe('-1'); + expect(result.title).toBe('-1'); + }); + + it('appends unit to the value when unit is provided and is not "bytes"', () => { + const result = getConfigDisplayValue(false, '100', 'ms'); + expect(result.displayValue).toBe('100 ms'); + expect(result.title).toBe('100 ms'); + }); + + it('returns value as is when no unit is provided', () => { + const result = getConfigDisplayValue(false, 'testValue', undefined); + expect(result.displayValue).toBe('testValue'); + expect(result.title).toBe('testValue'); + }); +}); diff --git a/frontend/src/components/Brokers/Broker/Configs/lib/constants.ts b/frontend/src/components/Brokers/Broker/Configs/lib/constants.ts new file mode 100644 index 000000000..5a829323a --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/lib/constants.ts @@ -0,0 +1,22 @@ +import { ConfigSource } from 'generated-sources'; + +export const CONFIG_SOURCE_NAME_MAP: Record = { + [ConfigSource.DYNAMIC_TOPIC_CONFIG]: 'Dynamic topic config', + [ConfigSource.DYNAMIC_BROKER_LOGGER_CONFIG]: 'Dynamic broker logger config', + [ConfigSource.DYNAMIC_BROKER_CONFIG]: 'Dynamic broker config', + [ConfigSource.DYNAMIC_DEFAULT_BROKER_CONFIG]: 'Dynamic default broker config', + [ConfigSource.STATIC_BROKER_CONFIG]: 'Static broker config', + [ConfigSource.DEFAULT_CONFIG]: 'Default config', + [ConfigSource.UNKNOWN]: 'Unknown', +} as const; + +export const CONFIG_SOURCE_PRIORITY = { + [ConfigSource.DYNAMIC_TOPIC_CONFIG]: 1, + [ConfigSource.DYNAMIC_BROKER_LOGGER_CONFIG]: 1, + [ConfigSource.DYNAMIC_BROKER_CONFIG]: 1, + [ConfigSource.DYNAMIC_DEFAULT_BROKER_CONFIG]: 1, + [ConfigSource.STATIC_BROKER_CONFIG]: 2, + [ConfigSource.DEFAULT_CONFIG]: 3, + [ConfigSource.UNKNOWN]: 4, + UNHANDLED: 5, +} as const; diff --git a/frontend/src/components/Brokers/Broker/Configs/lib/types.ts b/frontend/src/components/Brokers/Broker/Configs/lib/types.ts new file mode 100644 index 000000000..1a726cad3 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/lib/types.ts @@ -0,0 +1,13 @@ +import { type BrokerConfig } from 'generated-sources'; + +export type BrokerConfigsTableRow = Pick< + BrokerConfig, + 'name' | 'value' | 'source' | 'isReadOnly' | 'isSensitive' +>; + +export type UpdateBrokerConfigCallback = ( + name: BrokerConfig['name'], + value: BrokerConfig['value'] +) => Promise; + +export type ConfigUnit = 'ms' | 'bytes'; diff --git a/frontend/src/components/Brokers/Broker/Configs/lib/utils.tsx b/frontend/src/components/Brokers/Broker/Configs/lib/utils.tsx new file mode 100644 index 000000000..d452a1ac8 --- /dev/null +++ b/frontend/src/components/Brokers/Broker/Configs/lib/utils.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { type BrokerConfig, ConfigSource } from 'generated-sources'; +import { createColumnHelper } from '@tanstack/react-table'; +import * as BrokerConfigTableComponents from 'components/Brokers/Broker/Configs/TableComponents/index'; +import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; + +import type { + BrokerConfigsTableRow, + ConfigUnit, + UpdateBrokerConfigCallback, +} from './types'; +import { CONFIG_SOURCE_NAME_MAP, CONFIG_SOURCE_PRIORITY } from './constants'; + +const getConfigFieldMatch = (field: string, query: string) => + field.toLocaleLowerCase().includes(query.toLocaleLowerCase()); + +const filterConfigsBySearchQuery = + (searchQuery: string) => (config: BrokerConfig) => { + const nameMatch = getConfigFieldMatch(config.name, searchQuery); + const valueMatch = + config.value && getConfigFieldMatch(config.value, searchQuery); + + return nameMatch ? true : valueMatch; + }; + +const getConfigSourcePriority = (source: ConfigSource): number => + CONFIG_SOURCE_PRIORITY[source]; + +const sortBrokersBySource = (a: BrokerConfig, b: BrokerConfig) => { + const priorityA = getConfigSourcePriority(a.source); + const priorityB = getConfigSourcePriority(b.source); + + return priorityA - priorityB; +}; + +export const getConfigTableData = ( + configs: BrokerConfig[], + searchQuery: string +) => + configs + .filter(filterConfigsBySearchQuery(searchQuery)) + .sort(sortBrokersBySource); + +export const getBrokerConfigsTableColumns = ( + onUpdateInputCell: UpdateBrokerConfigCallback +) => { + const columnHelper = createColumnHelper(); + + return [ + columnHelper.accessor('name', { header: 'Key' }), + columnHelper.accessor('value', { + header: 'Value', + cell: (props) => ( + + ), + }), + columnHelper.accessor('source', { + header: BrokerConfigTableComponents.ConfigSourceHeader, + cell: ({ getValue }) => CONFIG_SOURCE_NAME_MAP[getValue()], + }), + ]; +}; + +const unitPatterns = { + ms: /\.ms$/, + bytes: /\.bytes$/, +}; + +export const getConfigUnit = (configName: string): ConfigUnit | undefined => { + const found = Object.entries(unitPatterns).find(([, pattern]) => + pattern.test(configName) + ); + + return found ? (found[0] as ConfigUnit) : undefined; +}; + +export const getConfigDisplayValue = ( + isSensitive: boolean, + value: string, + unit: ConfigUnit | undefined +) => { + if (isSensitive) { + return { displayValue: '**********', title: 'Sensitive Value' }; + } + + if (unit === 'bytes') { + const intValue = parseInt(value, 10); + return { + displayValue: intValue > 0 ? : value, + title: intValue > 0 ? `Bytes: ${value}` : value.toString(), + }; + } + + return { + displayValue: unit ? `${value} ${unit}` : value, + title: unit ? `${value} ${unit}` : value, + }; +}; diff --git a/frontend/src/components/Connect/List/ListPage.tsx b/frontend/src/components/Connect/List/ListPage.tsx index 94ec8354c..5d61ff3c4 100644 --- a/frontend/src/components/Connect/List/ListPage.tsx +++ b/frontend/src/components/Connect/List/ListPage.tsx @@ -1,21 +1,23 @@ import React, { Suspense } from 'react'; import useAppParams from 'lib/hooks/useAppParams'; -import { clusterConnectorNewRelativePath, ClusterNameRoute } from 'lib/paths'; +import { ClusterNameRoute, clusterConnectorNewRelativePath } from 'lib/paths'; import ClusterContext from 'components/contexts/ClusterContext'; import Search from 'components/common/Search/Search'; import * as Metrics from 'components/common/Metrics'; import PageHeading from 'components/common/PageHeading/PageHeading'; -import { ActionButton } from 'components/common/ActionComponent'; +import Tooltip from 'components/common/Tooltip/Tooltip'; import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled'; import PageLoader from 'components/common/PageLoader/PageLoader'; -import { Action, ConnectorState, ResourceType } from 'generated-sources'; -import { useConnectors } from 'lib/hooks/api/kafkaConnect'; +import { ConnectorState, Action, ResourceType } from 'generated-sources'; +import { useConnectors, useConnects } from 'lib/hooks/api/kafkaConnect'; +import { ActionButton } from 'components/common/ActionComponent'; import List from './List'; const ListPage: React.FC = () => { const { isReadOnly } = React.useContext(ClusterContext); const { clusterName } = useAppParams(); + const { data: connects = [] } = useConnects(clusterName); // Fetches all connectors from the API, without search criteria. Used to display general metrics. const { data: connectorsMetrics, isLoading } = useConnectors(clusterName); @@ -33,17 +35,25 @@ const ListPage: React.FC = () => { <> {!isReadOnly && ( - - Create Connector - + + Create Connector + + } + showTooltip={!connects.length} + content="No Connects available" + placement="left" + /> )} diff --git a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx index 7da8e47e8..0cbe4dd78 100644 --- a/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx +++ b/frontend/src/components/Connect/List/__tests__/ListPage.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { connectors } from 'lib/fixtures/kafkaConnect'; +import { connectors, connects } from 'lib/fixtures/kafkaConnect'; import ClusterContext, { ContextProps, initialValue, @@ -8,7 +8,7 @@ import ListPage from 'components/Connect/List/ListPage'; import { screen, within } from '@testing-library/react'; import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectorsPath } from 'lib/paths'; -import { useConnectors } from 'lib/hooks/api/kafkaConnect'; +import { useConnectors, useConnects } from 'lib/hooks/api/kafkaConnect'; jest.mock('components/Connect/List/List', () => () => (
Connectors List
@@ -16,6 +16,7 @@ jest.mock('components/Connect/List/List', () => () => ( jest.mock('lib/hooks/api/kafkaConnect', () => ({ useConnectors: jest.fn(), + useConnects: jest.fn(), })); jest.mock('components/common/Icons/SpinnerIcon', () => () => 'progressbar'); @@ -28,6 +29,10 @@ describe('Connectors List Page', () => { isLoading: false, data: [], })); + + (useConnects as jest.Mock).mockImplementation(() => ({ + data: connects, + })); }); const renderComponent = async (contextValue: ContextProps = initialValue) => @@ -178,4 +183,22 @@ describe('Connectors List Page', () => { expect(failedTasksIndicator).toHaveTextContent('Failed Tasks 1'); }); }); + + describe('Create new connector', () => { + it('Create new connector button is enabled when connects list is not empty', async () => { + await renderComponent(); + + expect(screen.getByText('Create Connector')).toBeEnabled(); + }); + + it('Create new connector button is disabled when connects list is empty', async () => { + (useConnects as jest.Mock).mockImplementation(() => ({ + data: [], + })); + + await renderComponent(); + + expect(screen.getByText('Create Connector')).toBeDisabled(); + }); + }); }); diff --git a/frontend/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx b/frontend/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx index 7456b5d2d..900b11e1e 100644 --- a/frontend/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx +++ b/frontend/src/components/KsqlDb/Query/QueryForm/QueryForm.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { FormError } from 'components/common/Input/Input.styled'; import { ErrorMessage } from '@hookform/error-message'; import { - useForm, Controller, - useFieldArray, FormProvider, + useFieldArray, + useForm, } from 'react-hook-form'; import { Button } from 'components/common/Button/Button'; import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; @@ -24,6 +24,7 @@ interface QueryFormProps { resetResults: () => void; submitHandler: (values: FormValues) => void; } + type StreamsPropertiesType = { key: string; value: string; @@ -118,7 +119,6 @@ const QueryForm: React.FC = ({ onClick={() => setValue('ksql', '')} buttonType="primary" buttonSize="S" - isInverted > Clear diff --git a/frontend/src/components/NavBar/NavBar.styled.ts b/frontend/src/components/NavBar/NavBar.styled.ts index d9013dcdd..6f47f7223 100644 --- a/frontend/src/components/NavBar/NavBar.styled.ts +++ b/frontend/src/components/NavBar/NavBar.styled.ts @@ -1,7 +1,8 @@ import styled, { css } from 'styled-components'; import { Link } from 'react-router-dom'; import DiscordIcon from 'components/common/Icons/DiscordIcon'; -import GitIcon from 'components/common/Icons/GitIcon'; +import GitHubIcon from 'components/common/Icons/GitHubIcon'; +import ProductHuntIcon from 'components/common/Icons/ProductHuntIcon'; export const Navbar = styled.nav( ({ theme }) => css` @@ -39,8 +40,12 @@ export const SocialLink = styled.a( fill: ${icons.discord.hover}; } - ${GitIcon} { - fill: ${icons.git.hover}; + ${GitHubIcon} { + fill: ${icons.github.hover}; + } + + ${ProductHuntIcon} { + fill: ${icons.producthunt.hover}; } } @@ -49,8 +54,12 @@ export const SocialLink = styled.a( fill: ${icons.discord.active}; } - ${GitIcon} { - fill: ${icons.git.active}; + ${GitHubIcon} { + fill: ${icons.github.active}; + } + + ${ProductHuntIcon} { + fill: ${icons.producthunt.active}; } } ` diff --git a/frontend/src/components/NavBar/NavBar.tsx b/frontend/src/components/NavBar/NavBar.tsx index 1a58c7e7b..a55be1dfa 100644 --- a/frontend/src/components/NavBar/NavBar.tsx +++ b/frontend/src/components/NavBar/NavBar.tsx @@ -2,12 +2,13 @@ import React, { useContext } from 'react'; import Select from 'components/common/Select/Select'; import Logo from 'components/common/Logo/Logo'; import Version from 'components/Version/Version'; -import GitIcon from 'components/common/Icons/GitIcon'; +import GitHubIcon from 'components/common/Icons/GitHubIcon'; import DiscordIcon from 'components/common/Icons/DiscordIcon'; import AutoIcon from 'components/common/Icons/AutoIcon'; import SunIcon from 'components/common/Icons/SunIcon'; import MoonIcon from 'components/common/Icons/MoonIcon'; import { ThemeModeContext } from 'components/contexts/ThemeModeContext'; +import ProductHuntIcon from 'components/common/Icons/ProductHuntIcon'; import UserInfo from './UserInfo/UserInfo'; import * as S from './NavBar.styled'; @@ -85,7 +86,7 @@ const NavBar: React.FC = ({ onBurgerClick }) => { isThemeMode /> - + = ({ onBurgerClick }) => { > + + + diff --git a/frontend/src/components/NavBar/UserInfo/UserInfo.styled.ts b/frontend/src/components/NavBar/UserInfo/UserInfo.styled.ts index 85154d767..2f99fb293 100644 --- a/frontend/src/components/NavBar/UserInfo/UserInfo.styled.ts +++ b/frontend/src/components/NavBar/UserInfo/UserInfo.styled.ts @@ -1,19 +1,23 @@ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; export const Wrapper = styled.div` display: flex; justify-content: center; align-items: center; gap: 5px; + svg { position: relative; } `; -export const Text = styled.div( - ({ theme }) => css` - color: ${theme.button.primary.invertedColors.normal}; - ` -); +export const Text = styled.div` + color: ${({ theme }) => theme.user.color}; + + &:hover { + color: ${({ theme }) => theme.user.hoverColor}; + } +} +`; export const LogoutLink = styled.a``; diff --git a/frontend/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx b/frontend/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx index f09eec6c5..8fd700c74 100644 --- a/frontend/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx +++ b/frontend/src/components/Topics/shared/Form/__tests__/TimeToRetainBtn.spec.tsx @@ -4,7 +4,7 @@ import { screen } from '@testing-library/react'; import TimeToRetainBtn, { Props, } from 'components/Topics/shared/Form/TimeToRetainBtn'; -import { useForm, FormProvider } from 'react-hook-form'; +import { FormProvider, useForm } from 'react-hook-form'; import { theme } from 'theme/theme'; import userEvent from '@testing-library/user-event'; @@ -61,7 +61,7 @@ describe('TimeToRetainBtn', () => { SetUpComponent({ value: 604800000 }); const buttonElement = screen.getByRole('button'); expect(buttonElement).toHaveStyle( - `background-color:${theme.button.secondary.invertedColors.normal}` + `background-color:${theme.chips.backgroundColor.active}` ); expect(buttonElement).toHaveStyle(`border:none`); }); diff --git a/frontend/src/components/common/Button/Button.styled.ts b/frontend/src/components/common/Button/Button.styled.ts index a436d01e7..649719987 100644 --- a/frontend/src/components/common/Button/Button.styled.ts +++ b/frontend/src/components/common/Button/Button.styled.ts @@ -3,7 +3,6 @@ import styled from 'styled-components'; export interface ButtonProps { buttonType: 'primary' | 'secondary' | 'danger'; buttonSize: 'S' | 'M' | 'L'; - isInverted?: boolean; } const StyledButton = styled.button` @@ -11,44 +10,32 @@ const StyledButton = styled.button` flex-direction: row; align-items: center; justify-content: center; - padding: 0 12px; + padding: ${({ buttonSize }) => (buttonSize === 'S' ? '0 8px' : '0 12px')}; border: none; border-radius: 4px; white-space: nowrap; - background: ${({ isInverted, buttonType, theme }) => - isInverted - ? 'transparent' - : theme.button[buttonType].backgroundColor.normal}; - color: ${({ isInverted, buttonType, theme }) => - isInverted - ? theme.button[buttonType].invertedColors.normal - : theme.button[buttonType].color.normal}; + background: ${({ buttonType, theme }) => + theme.button[buttonType].backgroundColor.normal}; + + color: ${({ buttonType, theme }) => theme.button[buttonType].color.normal}; + height: ${({ theme, buttonSize }) => theme.button.height[buttonSize]}; font-size: ${({ theme, buttonSize }) => theme.button.fontSize[buttonSize]}; font-weight: 500; - height: ${({ theme, buttonSize }) => theme.button.height[buttonSize]}; &:hover:enabled { - background: ${({ isInverted, buttonType, theme }) => - isInverted - ? 'transparent' - : theme.button[buttonType].backgroundColor.hover}; - color: ${({ isInverted, buttonType, theme }) => - isInverted - ? theme.button[buttonType].invertedColors.hover - : theme.button[buttonType].color}; + background: ${({ buttonType, theme }) => + theme.button[buttonType].backgroundColor.hover}; + color: ${({ buttonType, theme }) => theme.button[buttonType].color.normal}; cursor: pointer; } + &:active:enabled { - background: ${({ isInverted, buttonType, theme }) => - isInverted - ? 'transparent' - : theme.button[buttonType].backgroundColor.active}; - color: ${({ isInverted, buttonType, theme }) => - isInverted - ? theme.button[buttonType].invertedColors.active - : theme.button[buttonType].color}; + background: ${({ buttonType, theme }) => + theme.button[buttonType].backgroundColor.active}; + color: ${({ buttonType, theme }) => theme.button[buttonType].color.normal}; } + &:disabled { opacity: 0.5; cursor: not-allowed; @@ -59,11 +46,11 @@ const StyledButton = styled.button` } & a { - color: ${({ theme }) => theme.button.primary.color}; + color: ${({ theme }) => theme.button.primary.color.normal}; } & svg { - margin-right: 7px; + margin-right: 4px; fill: ${({ theme, disabled, buttonType }) => disabled ? theme.button[buttonType].color.disabled diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx index fe330a5e4..46bf120fe 100644 --- a/frontend/src/components/common/Button/Button.tsx +++ b/frontend/src/components/common/Button/Button.tsx @@ -1,10 +1,9 @@ -import StyledButton, { - ButtonProps, -} from 'components/common/Button/Button.styled'; import React from 'react'; import { Link } from 'react-router-dom'; import Spinner from 'components/common/Spinner/Spinner'; +import StyledButton, { ButtonProps } from './Button.styled'; + export interface Props extends React.ButtonHTMLAttributes, ButtonProps { @@ -12,24 +11,27 @@ export interface Props inProgress?: boolean; } -export const Button: React.FC = ({ to, ...props }) => { +export const Button: React.FC = ({ + to, + children, + disabled, + inProgress, + ...props +}) => { if (to) { return ( - - {props.children} + + {children} ); } + return ( - - {props.children}{' '} - {props.inProgress ? ( + + {children}{' '} + {inProgress ? ( ) : null} diff --git a/frontend/src/components/common/Button/__tests__/Button.spec.tsx b/frontend/src/components/common/Button/__tests__/Button.spec.tsx index 21919eb0d..ced6d0af4 100644 --- a/frontend/src/components/common/Button/__tests__/Button.spec.tsx +++ b/frontend/src/components/common/Button/__tests__/Button.spec.tsx @@ -50,14 +50,6 @@ describe('Button', () => { ); }); - it('renders inverted color Button', () => { - render( diff --git a/frontend/src/components/common/Icons/GitIcon.tsx b/frontend/src/components/common/Icons/GitHubIcon.tsx similarity index 90% rename from frontend/src/components/common/Icons/GitIcon.tsx rename to frontend/src/components/common/Icons/GitHubIcon.tsx index daecb611f..e9132c76f 100644 --- a/frontend/src/components/common/Icons/GitIcon.tsx +++ b/frontend/src/components/common/Icons/GitHubIcon.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from 'styled-components'; -const GitIcon: React.FC<{ className?: string }> = ({ className }) => ( +const GitHubIcon: React.FC<{ className?: string }> = ({ className }) => ( = ({ className }) => ( ); -export default styled(GitIcon)``; +export default styled(GitHubIcon)``; diff --git a/frontend/src/components/common/Icons/ProductHuntIcon.tsx b/frontend/src/components/common/Icons/ProductHuntIcon.tsx new file mode 100644 index 000000000..b0f660491 --- /dev/null +++ b/frontend/src/components/common/Icons/ProductHuntIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styled from 'styled-components'; + +const ProductHuntIcon: React.FC<{ className?: string }> = ({ className }) => ( + + + + +); + +export default styled(ProductHuntIcon)``; diff --git a/frontend/src/components/common/NewTable/__test__/Table.spec.tsx b/frontend/src/components/common/NewTable/__test__/Table.spec.tsx index b22e7c0ec..e31a88846 100644 --- a/frontend/src/components/common/NewTable/__test__/Table.spec.tsx +++ b/frontend/src/components/common/NewTable/__test__/Table.spec.tsx @@ -94,7 +94,7 @@ const columns: ColumnDef[] = [ const ExpandedRow: React.FC = () =>
I am expanded row
; -interface Props extends TableProps { +interface Props extends TableProps { path?: string; } diff --git a/frontend/src/components/common/Tooltip/Tooltip.tsx b/frontend/src/components/common/Tooltip/Tooltip.tsx index 0764320f5..6b74391ca 100644 --- a/frontend/src/components/common/Tooltip/Tooltip.tsx +++ b/frontend/src/components/common/Tooltip/Tooltip.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; import { + Placement, useFloating, useHover, useInteractions, - Placement, } from '@floating-ui/react'; import * as S from './Tooltip.styled'; @@ -12,9 +12,15 @@ interface TooltipProps { value: React.ReactNode; content: string; placement?: Placement; + showTooltip?: boolean; } -const Tooltip: React.FC = ({ value, content, placement }) => { +const Tooltip: React.FC = ({ + value, + content, + placement, + showTooltip = true, +}) => { const [open, setOpen] = useState(false); const { x, y, refs, strategy, context } = useFloating({ open, @@ -28,7 +34,7 @@ const Tooltip: React.FC = ({ value, content, placement }) => {
{value}
- {open && ( + {showTooltip && open && ( void; - setContent: React.Dispatch>; - setConfirm: React.Dispatch void) | undefined>>; + setContent: Dispatch>; + setConfirm: Dispatch void) | undefined>>; cancel: () => void; dangerButton: boolean; - setDangerButton: React.Dispatch>; + setDangerButton: Dispatch>; + isConfirming: boolean; + setIsConfirming: Dispatch>; } -export const ConfirmContext = React.createContext( - null -); +export const ConfirmContext = createContext(null); -export const ConfirmContextProvider: React.FC< - React.PropsWithChildren -> = ({ children }) => { - const [content, setContent] = useState(null); +export const ConfirmContextProvider: FC = ({ children }) => { + const [content, setContent] = useState(null); const [confirm, setConfirm] = useState<(() => void) | undefined>(undefined); const [dangerButton, setDangerButton] = useState(false); + const [isConfirming, setIsConfirming] = useState(false); const cancel = () => { setContent(null); @@ -36,6 +43,8 @@ export const ConfirmContextProvider: React.FC< cancel, dangerButton, setDangerButton, + isConfirming, + setIsConfirming, }} > {children} diff --git a/frontend/src/lib/hooks/useConfirm.ts b/frontend/src/lib/hooks/useConfirm.ts index baac856c5..117db2401 100644 --- a/frontend/src/lib/hooks/useConfirm.ts +++ b/frontend/src/lib/hooks/useConfirm.ts @@ -1,17 +1,22 @@ import { ConfirmContext } from 'components/contexts/ConfirmContext'; -import React, { useContext } from 'react'; +import { type ReactNode, useContext } from 'react'; export const useConfirm = (danger = false) => { const context = useContext(ConfirmContext); - return ( - message: React.ReactNode, - callback: () => void | Promise - ) => { + + return (message: ReactNode, callback: () => void | Promise) => { context?.setDangerButton(danger); context?.setContent(message); + context?.setIsConfirming(false); context?.setConfirm(() => async () => { - await callback(); - context?.cancel(); + context?.setIsConfirming(true); + + try { + await callback(); + } finally { + context?.setIsConfirming(false); + context?.cancel(); + } }); }; }; diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts deleted file mode 100644 index 1f42c255e..000000000 --- a/frontend/src/react-app-env.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// -/// -/// diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index 44e19eb5b..084e50f4e 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -35,15 +35,21 @@ const Colors = { '60': '#29A352', }, brand: { + '0': '#FFFFFF', + '3': '#F9FAFA', '5': '#F1F2F3', '10': '#E3E6E8', '15': '#D5DADD', '20': '#C7CED1', '30': '#ABB5BA', '40': '#8F9CA3', - '50': '#2F3639', - '60': '#22282A', - '70': '#171A1C', + '50': '#73848C', + '60': '#5C6970', + '70': '#454F54', + '80': '#2F3639', + '85': '#22282A', + '90': '#171A1C', + '95': '#0B0D0E', }, red: { '10': '#FAD1D1', @@ -203,14 +209,19 @@ const baseTheme = { closeModalIcon: Colors.neutral[25], savedIcon: Colors.brand[50], dropdownArrowIcon: Colors.neutral[50], - git: { + github: { hover: Colors.neutral[90], active: Colors.neutral[70], }, discord: { normal: Colors.neutral[20], hover: Colors.blue[45], - active: Colors.brand[15], + active: '#B8BEF9', + }, + producthunt: { + normal: Colors.neutral[20], + hover: '#FF6154', + active: '#FFBDB8', }, }, textArea: { @@ -335,6 +346,10 @@ export const theme = { color: Colors.brand[50], hoverColor: Colors.brand[60], }, + user: { + color: Colors.brand[70], + hoverColor: Colors.brand[50], + }, hr: { backgroundColor: Colors.neutral[5], }, @@ -391,38 +406,26 @@ export const theme = { button: { primary: { backgroundColor: { - normal: Colors.brand[50], - hover: Colors.brand[70], - active: Colors.brand[60], - disabled: Colors.neutral[5], + normal: Colors.brand[80], + hover: Colors.brand[90], + active: Colors.brand[70], + disabled: Colors.brand[50], }, color: { - normal: Colors.neutral[0], - disabled: Colors.neutral[30], - }, - invertedColors: { - normal: Colors.brand[50], - hover: Colors.brand[60], - active: Colors.brand[60], + normal: Colors.brand[0], + disabled: Colors.brand[30], }, }, secondary: { backgroundColor: { normal: Colors.brand[5], hover: Colors.brand[10], - active: Colors.brand[30], - disabled: Colors.neutral[5], + active: Colors.brand[15], + disabled: Colors.brand[5], }, color: { - normal: Colors.neutral[90], - disabled: Colors.neutral[30], - }, - isActiveColor: Colors.neutral[0], - invertedColors: { - normal: Colors.neutral[50], - hover: Colors.neutral[70], - active: Colors.neutral[90], - disabled: Colors.neutral[75], + normal: Colors.brand[90], + disabled: Colors.brand[30], }, }, danger: { @@ -433,13 +436,8 @@ export const theme = { disabled: Colors.red[20], }, color: { - normal: Colors.neutral[0], - disabled: Colors.neutral[0], - }, - invertedColors: { - normal: Colors.brand[50], - hover: Colors.brand[60], - active: Colors.brand[60], + normal: Colors.brand[0], + disabled: Colors.red[10], }, }, height: { @@ -452,11 +450,6 @@ export const theme = { M: '14px', L: '16px', }, - border: { - normal: Colors.neutral[50], - hover: Colors.neutral[70], - active: Colors.neutral[90], - }, }, chips: { backgroundColor: { @@ -781,6 +774,10 @@ export const darkTheme: ThemeType = { color: Colors.brand[50], hoverColor: Colors.brand[30], }, + user: { + color: Colors.brand[20], + hoverColor: Colors.brand[50], + }, hr: { backgroundColor: Colors.neutral[80], }, @@ -838,37 +835,25 @@ export const darkTheme: ThemeType = { primary: { backgroundColor: { normal: Colors.brand[10], - hover: Colors.brand[5], + hover: Colors.brand[0], active: Colors.brand[20], - disabled: Colors.brand[60], + disabled: Colors.brand[50], }, color: { - normal: Colors.neutral[70], - disabled: Colors.neutral[60], - }, - invertedColors: { - normal: Colors.brand[30], - hover: Colors.brand[60], - active: Colors.brand[60], + normal: Colors.brand[90], + disabled: Colors.brand[70], }, }, secondary: { backgroundColor: { - normal: Colors.brand[50], + normal: Colors.brand[80], hover: Colors.brand[70], active: Colors.brand[60], - disabled: Colors.neutral[75], + disabled: Colors.brand[80], }, color: { - normal: Colors.neutral[0], - disabled: Colors.neutral[60], - }, - isActiveColor: Colors.neutral[90], - invertedColors: { - normal: Colors.neutral[50], - hover: Colors.neutral[70], - active: Colors.neutral[90], - disabled: Colors.neutral[75], + normal: Colors.brand[0], + disabled: Colors.brand[70], }, }, danger: { @@ -879,13 +864,8 @@ export const darkTheme: ThemeType = { disabled: Colors.red[20], }, color: { - normal: Colors.neutral[0], - disabled: Colors.neutral[0], - }, - invertedColors: { - normal: Colors.brand[50], - hover: Colors.brand[60], - active: Colors.brand[60], + normal: Colors.brand[0], + disabled: Colors.red[10], }, }, height: { @@ -898,11 +878,6 @@ export const darkTheme: ThemeType = { M: '14px', L: '16px', }, - border: { - normal: Colors.neutral[50], - hover: Colors.neutral[70], - active: Colors.neutral[90], - }, }, chips: { backgroundColor: { @@ -1202,15 +1177,19 @@ export const darkTheme: ThemeType = { sunIcon: Colors.neutral[0], infoIcon: Colors.neutral[70], savedIcon: Colors.brand[30], - git: { - ...baseTheme.icons.git, + github: { + ...baseTheme.icons.github, hover: Colors.neutral[70], - active: Colors.neutral[90], + active: Colors.neutral[85], }, discord: { ...baseTheme.icons.discord, normal: Colors.neutral[30], }, + producthunt: { + ...baseTheme.icons.producthunt, + normal: Colors.neutral[5], + }, }, textArea: { ...baseTheme.textArea, diff --git a/serde-api/pom.xml b/serde-api/pom.xml index 9452dd3ac..3c2e3b3a1 100644 --- a/serde-api/pom.xml +++ b/serde-api/pom.xml @@ -1,128 +1,129 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - 4.0.0 - jar + 4.0.0 + jar - - 17 - 17 - + + 17 + 17 + - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + - kafbat-ui-serde-api - kafbat-ui-serde-api - http://github.com/kafbat/kafka-ui - - - The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - Kafbat - maintainers@kafbat.io - Kafbat - https://kafbat.io/ - - - - scm:git:git://github.com/kafbat/kafka-ui.git - scm:git:ssh://github.com:kafbat/kafka-ui.git - https://github.com/kafbat/kafka-ui - - io.kafbat.ui - serde-api - 1.0.0 + kafbat-ui-serde-api + kafbat-ui-serde-api + http://github.com/kafbat/kafka-ui + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + Kafbat + maintainers@kafbat.io + Kafbat + https://kafbat.io/ + + + + scm:git:git://github.com/kafbat/kafka-ui.git + scm:git:ssh://github.com:kafbat/kafka-ui.git + https://github.com/kafbat/kafka-ui + + io.kafbat.ui + serde-api + 1.0.0 - - - - - org.apache.maven.plugins - maven-install-plugin - 2.5.2 - - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 - true - - ossrh - https://s01.oss.sonatype.org/ - true - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - 8 - - 3.5.0 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - - - - --pinentry-mode - loopback - - - - - - + + + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + org.sonatype.central + central-publishing-maven-plugin + 0.4.0 + true + + central + true + true + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + 8 + + 3.5.0 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.1.0 + + + sign-artifacts + verify + + sign + + + + + + + --pinentry-mode + loopback + + + + + + diff --git a/settings.xml b/settings.xml index 7935a5f53..e3dd1641d 100644 --- a/settings.xml +++ b/settings.xml @@ -1,7 +1,7 @@ - ossrh + central ${server.username} ${server.password} @@ -14,4 +14,4 @@ - \ No newline at end of file +