@@ -104,7 +81,7 @@ const MessagesTable: React.FC = () => {
contentFilters={contentFilters}
/>
))}
- {isFetching && isLive && !messages.length && (
+ {isFetching && !messages.length && (
@@ -121,18 +98,10 @@ const MessagesTable: React.FC = () => {
- ← Back
-
-
Next →
diff --git a/frontend/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx b/frontend/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx
deleted file mode 100644
index 127a602f8..000000000
--- a/frontend/src/components/Topics/Topic/Messages/__test__/FiltersContainer.spec.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-import FiltersContainer from 'components/Topics/Topic/Messages/Filters/FiltersContainer';
-import { screen } from '@testing-library/react';
-import { render } from 'lib/testHelpers';
-
-jest.mock('components/Topics/Topic/Messages/Filters/Filters', () => () => (
- mock-Filters
-));
-
-describe('FiltersContainer', () => {
- it('renders Filters component', () => {
- render( );
- expect(screen.getByText('mock-Filters')).toBeInTheDocument();
- });
-});
diff --git a/frontend/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx b/frontend/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx
index 172dc8101..02c00f765 100644
--- a/frontend/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx
+++ b/frontend/src/components/Topics/Topic/Messages/__test__/Messages.spec.tsx
@@ -1,107 +1,46 @@
import React from 'react';
-import { screen, waitFor } from '@testing-library/react';
-import { render, EventSourceMock, WithRoute } from 'lib/testHelpers';
-import Messages, {
- SeekDirectionOptions,
- SeekDirectionOptionsObj,
-} from 'components/Topics/Topic/Messages/Messages';
-import { SeekDirection, SeekType } from 'generated-sources';
-import userEvent from '@testing-library/user-event';
-import { clusterTopicMessagesPath } from 'lib/paths';
-import { useSerdes } from 'lib/hooks/api/topicMessages';
-import { serdesPayload } from 'lib/fixtures/topicMessages';
-import { useTopicDetails } from 'lib/hooks/api/topics';
-import { externalTopicPayload } from 'lib/fixtures/topics';
+import { render } from 'lib/testHelpers';
+import Messages from 'components/Topics/Topic/Messages/Messages';
+import { useTopicMessages } from 'lib/hooks/api/topicMessages';
+import { screen } from '@testing-library/react';
+
+const mockFilterComponents = 'mockFilterComponents';
+const mockMessagesTable = 'mockMessagesTable';
jest.mock('lib/hooks/api/topicMessages', () => ({
- useSerdes: jest.fn(),
+ useTopicMessages: jest.fn(),
}));
-jest.mock('lib/hooks/api/topics', () => ({
- useTopicDetails: jest.fn(),
-}));
+jest.mock('components/Topics/Topic/Messages/MessagesTable', () => () => (
+ {mockMessagesTable}
+));
+
+jest.mock('components/Topics/Topic/Messages/Filters/Filters', () => () => (
+ {mockFilterComponents}
+));
describe('Messages', () => {
- const searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`;
- const renderComponent = (param: string = searchParams) => {
- const query = new URLSearchParams(param).toString();
- const path = `${clusterTopicMessagesPath()}?${query}`;
- return render(
-
-
- ,
- {
- initialEntries: [path],
- }
- );
+ const renderComponent = () => {
+ return render( );
};
beforeEach(() => {
- Object.defineProperty(window, 'EventSource', {
- value: EventSourceMock,
- });
- (useSerdes as jest.Mock).mockImplementation(() => ({
- data: serdesPayload,
- }));
- (useTopicDetails as jest.Mock).mockImplementation(() => ({
- data: externalTopicPayload,
+ (useTopicMessages as jest.Mock).mockImplementation(() => ({
+ data: { messages: [], isFetching: false },
}));
});
+
describe('component rendering default behavior with the search params', () => {
beforeEach(() => {
renderComponent();
});
- it('should check default seekDirection if it actually take the value from the url', () => {
- expect(screen.getAllByRole('listbox')[3]).toHaveTextContent(
- SeekDirectionOptionsObj[SeekDirection.FORWARD].label
- );
- });
- it('should check the SeekDirection select changes with live option', async () => {
- const seekDirectionSelect = screen.getAllByRole('listbox')[3];
- const seekDirectionOption = screen.getAllByRole('option')[3];
-
- expect(seekDirectionOption).toHaveTextContent(
- SeekDirectionOptionsObj[SeekDirection.FORWARD].label
- );
-
- const labelValue1 = SeekDirectionOptions[1].label;
- await userEvent.click(seekDirectionSelect);
- await userEvent.selectOptions(seekDirectionSelect, [labelValue1]);
- expect(seekDirectionOption).toHaveTextContent(labelValue1);
-
- const labelValue0 = SeekDirectionOptions[0].label;
- await userEvent.click(seekDirectionSelect);
- await userEvent.selectOptions(seekDirectionSelect, [labelValue0]);
- expect(seekDirectionOption).toHaveTextContent(labelValue0);
-
- const liveOptionConf = SeekDirectionOptions[2];
- const labelValue2 = liveOptionConf.label;
- await userEvent.click(seekDirectionSelect);
-
- const options = screen.getAllByRole('option');
- const liveModeLi = options.find(
- (option) => option.getAttribute('value') === liveOptionConf.value
- );
- expect(liveModeLi).toBeInTheDocument();
- if (!liveModeLi) return; // to make TS happy
- await userEvent.selectOptions(seekDirectionSelect, [liveModeLi]);
- expect(seekDirectionOption).toHaveTextContent(labelValue2);
-
- await waitFor(() => {
- expect(screen.getByRole('contentLoader')).toBeInTheDocument();
- });
+ it('should check if the filters are shown in the messages', () => {
+ expect(screen.getByText(mockFilterComponents)).toBeInTheDocument();
});
- });
- describe('Component rendering with custom Url search params', () => {
- it('reacts to a change of seekDirection in the url which make the select pick up different value', () => {
- renderComponent(
- searchParams.replace(SeekDirection.FORWARD, SeekDirection.BACKWARD)
- );
- expect(screen.getAllByRole('listbox')[3]).toHaveTextContent(
- SeekDirectionOptionsObj[SeekDirection.BACKWARD].label
- );
+ it('should check if the table of messages are shown in the messages', () => {
+ expect(screen.getByText(mockMessagesTable)).toBeInTheDocument();
});
});
});
diff --git a/frontend/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx b/frontend/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx
index 7b1e80f8c..d179a2100 100644
--- a/frontend/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx
+++ b/frontend/src/components/Topics/Topic/Messages/__test__/MessagesTable.spec.tsx
@@ -2,63 +2,41 @@ import React from 'react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
-import MessagesTable from 'components/Topics/Topic/Messages/MessagesTable';
-import { SeekDirection, SeekType, TopicMessage } from 'generated-sources';
-import TopicMessagesContext, {
- ContextProps,
-} from 'components/contexts/TopicMessagesContext';
-import {
- topicMessagePayload,
- topicMessagesMetaPayload,
-} from 'redux/reducers/topicMessages/__test__/fixtures';
-
-const mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }];
+import MessagesTable, {
+ MessagesTableProps,
+} from 'components/Topics/Topic/Messages/MessagesTable';
+import { TopicMessage, TopicMessageTimestampTypeEnum } from 'generated-sources';
+import { useIsLiveMode } from 'lib/hooks/useMessagesFilters';
+
+export const topicMessagePayload: TopicMessage = {
+ partition: 29,
+ offset: 14,
+ timestamp: new Date('2021-07-21T23:25:14.865Z'),
+ timestampType: TopicMessageTimestampTypeEnum.CREATE_TIME,
+ key: 'schema-registry',
+ headers: {},
+ content:
+ '{"host":"schemaregistry1","port":8085,"master_eligibility":true,"scheme":"http","version":1}',
+};
+
+const mockTopicsMessages = [{ ...topicMessagePayload }];
const mockNavigate = jest.fn();
+
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
-describe('MessagesTable', () => {
- const searchParams = new URLSearchParams({
- filterQueryType: 'STRING_CONTAINS',
- attempt: '0',
- limit: '100',
- seekDirection: SeekDirection.FORWARD,
- seekType: SeekType.OFFSET,
- seekTo: '0::9',
- });
- const contextValue: ContextProps = {
- isLive: false,
- seekDirection: SeekDirection.FORWARD,
- changeSeekDirection: jest.fn(),
- };
+jest.mock('lib/hooks/useMessagesFilters', () => ({
+ useIsLiveMode: jest.fn(),
+ useRefreshData: jest.fn(),
+}));
- const renderComponent = (
- params: URLSearchParams = searchParams,
- ctx: ContextProps = contextValue,
- messages: TopicMessage[] = [],
- isFetching?: boolean,
- path?: string
- ) => {
- const customPath = path || params.toString();
+describe('MessagesTable', () => {
+ const renderComponent = (props?: Partial) => {
return render(
-
-
- ,
- {
- initialEntries: [`/messages?${customPath}`],
- preloadedState: {
- topicMessages: {
- messages,
- meta: {
- ...topicMessagesMetaPayload,
- },
- isFetching: !!isFetching,
- },
- },
- }
+
);
};
@@ -90,33 +68,35 @@ describe('MessagesTable', () => {
});
describe('Custom Setup with different props value', () => {
- it('should check if next button and previous is disabled isLive Param', () => {
- renderComponent(searchParams, { ...contextValue, isLive: true });
+ it('should check if next button is disabled isLive Param', () => {
+ renderComponent({ isFetching: true });
+ expect(screen.queryByText(/next/i)).toBeDisabled();
+ });
+
+ it('should check if next button is disabled if there is no nextCursor', () => {
+ (useIsLiveMode as jest.Mock).mockImplementation(() => false);
+ renderComponent({ isFetching: false });
expect(screen.queryByText(/next/i)).toBeDisabled();
- expect(screen.queryByText(/back/i)).toBeDisabled();
});
- it('should check the display of the loader element', () => {
- renderComponent(
- searchParams,
- { ...contextValue, isLive: true },
- [],
- true
- );
+ it('should check the display of the loader element during loader', () => {
+ renderComponent({ isFetching: true });
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
});
describe('should render Messages table with data', () => {
beforeEach(() => {
- renderComponent(searchParams, { ...contextValue }, mockTopicsMessages);
+ renderComponent({ messages: mockTopicsMessages, isFetching: false });
});
it('should check the rendering of the messages', () => {
expect(screen.queryByText(/No messages found/i)).not.toBeInTheDocument();
- expect(
- screen.getByText(mockTopicsMessages[0].content as string)
- ).toBeInTheDocument();
+ if (mockTopicsMessages[0].content) {
+ expect(
+ screen.getByText(mockTopicsMessages[0].content)
+ ).toBeInTheDocument();
+ }
});
});
});
diff --git a/frontend/src/components/Topics/Topic/Messages/__test__/utils.spec.ts b/frontend/src/components/Topics/Topic/Messages/__test__/utils.spec.ts
index 97dd0ec7b..d0f00967d 100644
--- a/frontend/src/components/Topics/Topic/Messages/__test__/utils.spec.ts
+++ b/frontend/src/components/Topics/Topic/Messages/__test__/utils.spec.ts
@@ -1,11 +1,16 @@
import { Option } from 'react-multi-select-component';
import {
+ ADD_FILTER_ID,
filterOptions,
getOffsetFromSeekToParam,
- getTimestampFromSeekToParam,
getSelectedPartitionsFromSeekToParam,
+ getTimestampFromSeekToParam,
+ isEditingFilterMode,
+ isLiveMode,
+ isModeOffsetSelector,
+ isModeOptionWithInput,
} from 'components/Topics/Topic/Messages/Filters/utils';
-import { SeekType, Partition } from 'generated-sources';
+import { Partition, PollingMode, SeekType } from 'generated-sources';
const options: Option[] = [
{
@@ -117,4 +122,47 @@ describe('utils', () => {
]);
});
});
+
+ describe('isModeOptionWithInput', () => {
+ describe('check the validity if Mode offset Selector only during', () => {
+ expect(isModeOptionWithInput(PollingMode.TAILING)).toBeFalsy();
+ expect(isModeOptionWithInput(PollingMode.LATEST)).toBeFalsy();
+ expect(isModeOptionWithInput(PollingMode.EARLIEST)).toBeFalsy();
+ expect(isModeOptionWithInput(PollingMode.FROM_TIMESTAMP)).toBeTruthy();
+ expect(isModeOptionWithInput(PollingMode.TO_TIMESTAMP)).toBeTruthy();
+ expect(isModeOptionWithInput(PollingMode.FROM_OFFSET)).toBeTruthy();
+ expect(isModeOptionWithInput(PollingMode.TO_OFFSET)).toBeTruthy();
+ });
+ });
+
+ describe('isModeOffsetSelector', () => {
+ it('check the validity if Mode offset Selector only during', () => {
+ expect(isModeOffsetSelector(PollingMode.TAILING)).toBeFalsy();
+ expect(isModeOffsetSelector(PollingMode.LATEST)).toBeFalsy();
+ expect(isModeOffsetSelector(PollingMode.EARLIEST)).toBeFalsy();
+ expect(isModeOffsetSelector(PollingMode.FROM_TIMESTAMP)).toBeFalsy();
+ expect(isModeOffsetSelector(PollingMode.TO_TIMESTAMP)).toBeFalsy();
+ expect(isModeOffsetSelector(PollingMode.FROM_OFFSET)).toBeTruthy();
+ expect(isModeOffsetSelector(PollingMode.TO_OFFSET)).toBeTruthy();
+ });
+ });
+
+ describe('isLiveMode', () => {
+ it('should check the validity of data on;y during tailing mode', () => {
+ expect(isLiveMode(PollingMode.TAILING)).toBeTruthy();
+ expect(isLiveMode(PollingMode.LATEST)).toBeFalsy();
+ expect(isLiveMode(PollingMode.EARLIEST)).toBeFalsy();
+ expect(isLiveMode(PollingMode.FROM_TIMESTAMP)).toBeFalsy();
+ expect(isLiveMode(PollingMode.TO_TIMESTAMP)).toBeFalsy();
+ expect(isLiveMode(PollingMode.FROM_OFFSET)).toBeFalsy();
+ expect(isLiveMode(PollingMode.TO_OFFSET)).toBeFalsy();
+ });
+ });
+
+ describe('isEditingFilterMode', () => {
+ it('should editing value', () => {
+ expect(isEditingFilterMode('testing')).toBeTruthy();
+ expect(isEditingFilterMode(ADD_FILTER_ID)).toBeFalsy();
+ });
+ });
});
diff --git a/frontend/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/frontend/src/components/Topics/Topic/SendMessage/SendMessage.tsx
index bef7a4ddd..4bdb981f8 100644
--- a/frontend/src/components/Topics/Topic/SendMessage/SendMessage.tsx
+++ b/frontend/src/components/Topics/Topic/SendMessage/SendMessage.tsx
@@ -3,7 +3,7 @@ import { useForm, Controller } from 'react-hook-form';
import { RouteParamsClusterTopic } from 'lib/paths';
import { Button } from 'components/common/Button/Button';
import Editor from 'components/common/Editor/Editor';
-import Select, { SelectOption } from 'components/common/Select/Select';
+import Select from 'components/common/Select/Select';
import Switch from 'components/common/Switch/Switch';
import useAppParams from 'lib/hooks/useAppParams';
import { showAlert } from 'lib/errorHandling';
@@ -43,7 +43,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({
const sendMessage = useSendMessage({ clusterName, topicName });
const defaultValues = React.useMemo(() => getDefaultValues(serdes), [serdes]);
- const partitionOptions: SelectOption[] = React.useMemo(
+ const partitionOptions = React.useMemo(
() => getPartitionOptions(topic?.partitions || []),
[topic]
);
diff --git a/frontend/src/components/Topics/Topic/SendMessage/utils.ts b/frontend/src/components/Topics/Topic/SendMessage/utils.ts
index c8161b0c8..46d9e1278 100644
--- a/frontend/src/components/Topics/Topic/SendMessage/utils.ts
+++ b/frontend/src/components/Topics/Topic/SendMessage/utils.ts
@@ -4,7 +4,6 @@ import {
TopicSerdeSuggestion,
} from 'generated-sources';
import jsf from 'json-schema-faker';
-import { compact } from 'lodash';
import Ajv, { DefinedError } from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import upperFirst from 'lodash/upperFirst';
@@ -46,12 +45,12 @@ export const getPartitionOptions = (partitions: Partition[]) =>
}));
export const getSerdeOptions = (items: SerdeDescription[]) => {
- const options = items.map(({ name }) => {
- if (!name) return undefined;
- return { label: name, value: name };
- });
-
- return compact(options);
+ return items.reduce<{ label: string; value: string }[]>((acc, { name }) => {
+ if (name) {
+ acc.push({ value: name, label: name });
+ }
+ return acc;
+ }, []);
};
export const validateBySchema = (
diff --git a/frontend/src/components/Topics/Topic/Topic.tsx b/frontend/src/components/Topics/Topic/Topic.tsx
index 5a639f0c4..b5bcf8d52 100644
--- a/frontend/src/components/Topics/Topic/Topic.tsx
+++ b/frontend/src/components/Topics/Topic/Topic.tsx
@@ -17,7 +17,6 @@ import {
ActionDropdownItem,
} from 'components/common/ActionComponent';
import Navbar from 'components/common/Navigation/Navbar.styled';
-import { useAppDispatch } from 'lib/hooks/redux';
import useAppParams from 'lib/hooks/useAppParams';
import { Dropdown, DropdownItemHint } from 'components/common/Dropdown';
import {
@@ -26,7 +25,6 @@ import {
useRecreateTopic,
useTopicDetails,
} from 'lib/hooks/api/topics';
-import { resetTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import { Action, CleanUpPolicy, ResourceType } from 'generated-sources';
import PageLoader from 'components/common/PageLoader/PageLoader';
import SlidingSidebar from 'components/common/SlidingSidebar';
@@ -41,7 +39,6 @@ import Edit from './Edit/Edit';
import SendMessage from './SendMessage/SendMessage';
const Topic: React.FC = () => {
- const dispatch = useAppDispatch();
const {
value: isSidebarOpen,
setFalse: closeSidebar,
@@ -62,16 +59,12 @@ const Topic: React.FC = () => {
navigate(clusterTopicsPath(clusterName));
};
- React.useEffect(() => {
- return () => {
- dispatch(resetTopicMessages());
- };
- }, []);
const clearMessages = useClearTopicMessages(clusterName);
const clearTopicMessagesHandler = async () => {
await clearMessages.mutateAsync(topicName);
};
const canCleanup = data?.cleanUpPolicy === CleanUpPolicy.DELETE;
+
return (
<>
Promise;
}
-const CleanupPolicyOptions: Array = [
+const CleanupPolicyOptions = [
{ value: 'delete', label: 'Delete' },
{ value: 'compact', label: 'Compact' },
{ value: 'compact,delete', label: 'Compact,Delete' },
@@ -39,7 +39,7 @@ const CleanupPolicyOptions: Array = [
export const getCleanUpPolicyValue = (cleanUpPolicy?: string) => {
if (!cleanUpPolicy) return undefined;
- return CleanupPolicyOptions.find((option: SelectOption) => {
+ return CleanupPolicyOptions.find((option) => {
return (
option.value.toString().replace(/,/g, '_') ===
cleanUpPolicy?.toLowerCase()
@@ -47,7 +47,7 @@ export const getCleanUpPolicyValue = (cleanUpPolicy?: string) => {
})?.value.toString();
};
-const RetentionBytesOptions: Array = [
+const RetentionBytesOptions = [
{ value: NOT_SET, label: 'Not Set' },
{ value: BYTES_IN_GB, label: '1 GB' },
{ value: BYTES_IN_GB * 10, label: '10 GB' },
@@ -75,7 +75,7 @@ const TopicForm: React.FC = ({
getCleanUpPolicyValue(cleanUpPolicy) || CleanupPolicyOptions[0].value;
const getRetentionBytes =
- RetentionBytesOptions.find((option: SelectOption) => {
+ RetentionBytesOptions.find((option) => {
return option.value === retentionBytes;
})?.value || RetentionBytesOptions[0].value;
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/ActionComponent/ActionSelect/ActionSelect.tsx b/frontend/src/components/common/ActionComponent/ActionSelect/ActionSelect.tsx
index 31c73982a..030546187 100644
--- a/frontend/src/components/common/ActionComponent/ActionSelect/ActionSelect.tsx
+++ b/frontend/src/components/common/ActionComponent/ActionSelect/ActionSelect.tsx
@@ -8,15 +8,15 @@ import { useActionTooltip } from 'lib/hooks/useActionTooltip';
import { usePermission } from 'lib/hooks/usePermission';
import * as S from 'components/common/ActionComponent/ActionComponent.styled';
-interface Props extends SelectProps, ActionComponentProps {}
+interface Props extends SelectProps, ActionComponentProps {}
-const ActionSelect: React.FC = ({
+const ActionSelect = ({
message = getDefaultActionMessage(),
permission,
placement = 'bottom',
disabled,
...props
-}) => {
+}: Props) => {
const canDoAction = usePermission(
permission.resource,
permission.action,
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( );
- expect(screen.getByRole('button')).toBeInTheDocument();
- expect(screen.getByRole('button')).toHaveStyleRule(
- 'color',
- theme.button.primary.invertedColors.normal
- );
- });
it('renders disabled button and spinner when inProgress truthy', () => {
render( );
expect(screen.getByRole('button')).toBeInTheDocument();
diff --git a/frontend/src/components/common/ConfirmationModal/ConfirmationModal.tsx b/frontend/src/components/common/ConfirmationModal/ConfirmationModal.tsx
index 1b882c946..98a9a51eb 100644
--- a/frontend/src/components/common/ConfirmationModal/ConfirmationModal.tsx
+++ b/frontend/src/components/common/ConfirmationModal/ConfirmationModal.tsx
@@ -30,6 +30,7 @@ const ConfirmationModal: React.FC = () => {
buttonSize="M"
onClick={context.confirm}
type="button"
+ inProgress={context?.isConfirming}
>
Confirm
diff --git a/frontend/src/components/common/FlexBox/FlexBox.tsx b/frontend/src/components/common/FlexBox/FlexBox.tsx
new file mode 100644
index 000000000..2a767ad56
--- /dev/null
+++ b/frontend/src/components/common/FlexBox/FlexBox.tsx
@@ -0,0 +1,39 @@
+import React, { CSSProperties, ReactNode } from 'react';
+import styled from 'styled-components';
+
+interface FlexboxProps {
+ flexDirection?: CSSProperties['flexDirection'];
+ alignItems?: CSSProperties['alignItems'];
+ alignSelf?: CSSProperties['alignSelf'];
+ justifyContent?: CSSProperties['justifyContent'];
+ justifyItems?: CSSProperties['justifyItems'];
+ gap?: CSSProperties['gap'];
+ margin?: CSSProperties['margin'];
+ padding?: CSSProperties['padding'];
+ color?: CSSProperties['color'];
+ flexGrow?: CSSProperties['flexGrow'];
+ flexWrap?: CSSProperties['flexWrap'];
+ width?: CSSProperties['width'];
+ children: ReactNode;
+}
+
+const FlexboxContainer = styled.div`
+ display: flex;
+ flex-direction: ${(props) => props.flexDirection || 'row'};
+ align-items: ${(props) => props.alignItems};
+ align-self: ${(props) => props.alignSelf};
+ justify-content: ${(props) => props.justifyContent};
+ justify-items: ${(props) => props.justifyItems};
+ gap: ${(props) => props.gap};
+ margin: ${(props) => props.margin};
+ padding: ${(props) => props.padding};
+ flex-grow: ${(props) => props.flexGrow};
+ width: ${(props) => props.width};
+ color ${(props) => props.color};
+`;
+
+const Flexbox: React.FC = ({ children, ...rest }) => {
+ return {children} ;
+};
+
+export default Flexbox;
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..266276379 100644
--- a/frontend/src/components/common/NewTable/__test__/Table.spec.tsx
+++ b/frontend/src/components/common/NewTable/__test__/Table.spec.tsx
@@ -7,7 +7,7 @@ import Table, {
LinkCell,
TagCell,
} from 'components/common/NewTable';
-import { screen, waitFor } from '@testing-library/dom';
+import { screen } from '@testing-library/dom';
import { ColumnDef, Row } from '@tanstack/react-table';
import userEvent from '@testing-library/user-event';
import { formatTimestamp } from 'lib/dateTimeHelpers';
@@ -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;
}
@@ -276,7 +276,7 @@ describe('Table', () => {
describe('Sorting', () => {
it('sort rows', async () => {
- await renderComponent({
+ renderComponent({
path: '/?sortBy=text&&sortDirection=desc',
enableSorting: true,
});
@@ -293,7 +293,7 @@ describe('Table', () => {
expect(rows[1].textContent?.indexOf('sit')).toBeGreaterThan(-1);
// Disable sorting by text column
- await waitFor(() => userEvent.click(th));
+ await userEvent.click(th);
rows = screen.getAllByRole('row');
expect(rows[1].textContent?.indexOf('lorem')).toBeGreaterThan(-1);
expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);
@@ -301,7 +301,7 @@ describe('Table', () => {
expect(rows[4].textContent?.indexOf('sit')).toBeGreaterThan(-1);
// Sort by text column ascending
- await waitFor(() => userEvent.click(th));
+ await userEvent.click(th);
rows = screen.getAllByRole('row');
expect(rows[1].textContent?.indexOf('dolor')).toBeGreaterThan(-1);
expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);
diff --git a/frontend/src/components/common/Search/Search.tsx b/frontend/src/components/common/Search/Search.tsx
index 72b5d1d54..3fa7d27ac 100644
--- a/frontend/src/components/common/Search/Search.tsx
+++ b/frontend/src/components/common/Search/Search.tsx
@@ -1,4 +1,4 @@
-import React, { useRef } from 'react';
+import React, { ComponentRef, useRef } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import Input from 'components/common/Input/Input';
import { useSearchParams } from 'react-router-dom';
@@ -29,7 +29,8 @@ const Search: React.FC = ({
onChange,
}) => {
const [searchParams, setSearchParams] = useSearchParams();
- const ref = useRef(null);
+ const ref = useRef>(null);
+
const handleChange = useDebouncedCallback((e) => {
if (ref.current != null) {
ref.current.value = e.target.value;
@@ -44,8 +45,11 @@ const Search: React.FC = ({
setSearchParams(searchParams);
}
}, 500);
+
const clearSearchValue = () => {
- if (searchParams.get('q')) {
+ if (onChange) {
+ onChange('');
+ } else if (searchParams.get('q')) {
searchParams.set('q', '');
setSearchParams(searchParams);
}
diff --git a/frontend/src/components/common/Select/ControlledSelect.tsx b/frontend/src/components/common/Select/ControlledSelect.tsx
index 1ea90c356..54174fd4c 100644
--- a/frontend/src/components/common/Select/ControlledSelect.tsx
+++ b/frontend/src/components/common/Select/ControlledSelect.tsx
@@ -6,24 +6,24 @@ import { ErrorMessage } from '@hookform/error-message';
import Select, { SelectOption } from './Select';
-interface ControlledSelectProps {
+interface ControlledSelectProps {
name: string;
label: React.ReactNode;
hint?: string;
- options: SelectOption[];
- onChange?: (val: string | number) => void;
+ options: SelectOption[];
+ onChange?: (val: T) => void;
disabled?: boolean;
placeholder?: string;
}
-const ControlledSelect: React.FC = ({
+const ControlledSelect = ({
name,
label,
onChange,
options,
disabled = false,
placeholder,
-}) => {
+}: ControlledSelectProps) => {
const id = React.useId();
return (
diff --git a/frontend/src/components/common/Select/Select.tsx b/frontend/src/components/common/Select/Select.tsx
index a72660d2c..72bf358ba 100644
--- a/frontend/src/components/common/Select/Select.tsx
+++ b/frontend/src/components/common/Select/Select.tsx
@@ -3,123 +3,113 @@ import useClickOutside from 'lib/hooks/useClickOutside';
import DropdownArrowIcon from 'components/common/Icons/DropdownArrowIcon';
import * as S from './Select.styled';
-import LiveIcon from './LiveIcon.styled';
-export interface SelectProps {
- options?: Array;
+export interface SelectProps {
+ options?: SelectOption[];
id?: string;
name?: string;
selectSize?: 'M' | 'L';
- isLive?: boolean;
minWidth?: string;
- value?: string | number;
- defaultValue?: string | number;
+ value?: T;
+ defaultValue?: T;
placeholder?: string;
disabled?: boolean;
- onChange?: (option: string | number) => void;
+ onChange?: (option: T) => void;
isThemeMode?: boolean;
}
-export interface SelectOption {
+export interface SelectOption {
label: string | number | React.ReactElement;
- value: string | number;
+ value: T;
disabled?: boolean;
- isLive?: boolean;
}
-const Select = React.forwardRef(
- (
- {
- options = [],
- value,
- defaultValue,
- selectSize = 'L',
- placeholder = '',
- isLive,
- disabled = false,
- onChange,
- isThemeMode,
- ...props
- },
- ref
- ) => {
- const [selectedOption, setSelectedOption] = useState(value);
- const [showOptions, setShowOptions] = useState(false);
+// Use the generic type T for forwardRef
+const Select = (
+ {
+ options = [],
+ value,
+ defaultValue,
+ selectSize = 'L',
+ placeholder = '',
+ disabled = false,
+ onChange,
+ isThemeMode,
+ ...props
+ }: SelectProps,
+ ref?: React.Ref
+) => {
+ const [selectedOption, setSelectedOption] = useState(value);
+ const [showOptions, setShowOptions] = useState(false);
- const showOptionsHandler = () => {
- if (!disabled) setShowOptions(!showOptions);
- };
+ const showOptionsHandler = () => {
+ if (!disabled) setShowOptions(!showOptions);
+ };
- const selectContainerRef = useRef(null);
- const clickOutsideHandler = () => setShowOptions(false);
- useClickOutside(selectContainerRef, clickOutsideHandler);
+ const selectContainerRef = useRef(null);
+ const clickOutsideHandler = () => setShowOptions(false);
+ useClickOutside(selectContainerRef, clickOutsideHandler);
- const updateSelectedOption = (option: SelectOption) => {
- if (!option.disabled) {
- setSelectedOption(option.value);
+ const updateSelectedOption = (option: SelectOption) => {
+ if (!option.disabled) {
+ setSelectedOption(option.value);
- if (onChange) {
- onChange(option.value);
- }
-
- setShowOptions(false);
+ if (onChange) {
+ onChange(option.value);
}
- };
- React.useEffect(() => {
- setSelectedOption(value);
- }, [isLive, value]);
+ setShowOptions(false);
+ }
+ };
- return (
-