diff --git a/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts b/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts index 65cb171836..681958d5a2 100644 --- a/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts +++ b/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts @@ -6,4 +6,5 @@ export abstract class CommandExecutionRepository { abstract getList(databaseId: string): Promise; abstract getOne(databaseId: string, id: string): Promise; abstract delete(databaseId: string, id: string): Promise; + abstract deleteAll(databaseId: string): Promise; } diff --git a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts index 83985d9e35..fe7be1f6cf 100644 --- a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts +++ b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts @@ -253,6 +253,14 @@ describe('LocalCommandExecutionRepository', () => { ); }); }); + describe('deleteAll', () => { + it('Should not return anything on delete', async () => { + repository.delete.mockResolvedValueOnce(1); + expect(await service.deleteAll(mockDatabase.id)).toEqual( + undefined, + ); + }); + }); describe('cleanupDatabaseHistory', () => { it('Should should not return anything on cleanup', async () => { mockQueryBuilderGetManyRaw.mockReturnValueOnce([ diff --git a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts index 7ae9661203..1ac363bcf5 100644 --- a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts +++ b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts @@ -163,6 +163,19 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository this.logger.log('Command execution deleted'); } + /** + * Delete all items + * + * @param databaseId + */ + async deleteAll(databaseId: string): Promise { + this.logger.log('Delete all command executions'); + + await this.commandExecutionRepository.delete({ databaseId }); + + this.logger.log('Command executions deleted'); + } + /** * Clean history for particular database to fit 30 items limitation * @param databaseId diff --git a/redisinsight/api/src/modules/workbench/workbench.controller.ts b/redisinsight/api/src/modules/workbench/workbench.controller.ts index 37d59ec02b..f187658dc2 100644 --- a/redisinsight/api/src/modules/workbench/workbench.controller.ts +++ b/redisinsight/api/src/modules/workbench/workbench.controller.ts @@ -98,4 +98,16 @@ export class WorkbenchController { ): Promise { return this.service.deleteCommandExecution(databaseId, id); } + + @ApiEndpoint({ + description: 'Delete command executions', + statusCode: 200, + }) + @Delete('/command-executions') + @ApiRedisParams() + async deleteCommandExecutions( + @Param('dbInstance') databaseId: string, + ): Promise { + return this.service.deleteCommandExecutions(databaseId); + } } diff --git a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts index b2c40b446c..45a75a6820 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts @@ -134,6 +134,7 @@ const mockCommandExecutionRepository = () => ({ getList: jest.fn(), getOne: jest.fn(), delete: jest.fn(), + deleteAll: jest.fn(), }); describe('WorkbenchService', () => { @@ -389,6 +390,17 @@ describe('WorkbenchService', () => { mockCommandExecution.id, ); + expect(result).toEqual(undefined); + }); + }); + describe('deleteCommandExecutions', () => { + it('should not return anything on delete', async () => { + commandExecutionProvider.deleteAll.mockResolvedValueOnce('some response'); + + const result = await service.deleteCommandExecutions( + mockWorkbenchClientMetadata.databaseId, + ); + expect(result).toEqual(undefined); }); }); diff --git a/redisinsight/api/src/modules/workbench/workbench.service.ts b/redisinsight/api/src/modules/workbench/workbench.service.ts index 85a160d170..c73a6fa352 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.ts @@ -187,6 +187,15 @@ export class WorkbenchService { this.analyticsService.sendCommandDeletedEvent(databaseId); } + /** + * Delete command executions by databaseId + * + * @param databaseId + */ + async deleteCommandExecutions(databaseId: string): Promise { + await this.commandExecutionRepository.deleteAll(databaseId); + } + /** * Check if workbench allows such command * @param commandLine diff --git a/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts new file mode 100644 index 0000000000..671a25b733 --- /dev/null +++ b/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts @@ -0,0 +1,64 @@ +import { + expect, + describe, + it, + deps, + validateApiCall, +} from '../deps'; +const { server, request, constants, rte, localDb } = deps; + +// endpoint to test +const endpoint = ( + instanceId = constants.TEST_INSTANCE_ID, +) => + request(server).delete(`/${constants.API.DATABASES}/${instanceId}/workbench/command-executions`); + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('DELETE /databases/:instanceId/workbench/command-executions', () => { + describe('Common', () => { + [ + { + name: 'Should return 404 not found when incorrect instance', + endpoint: () => endpoint( + constants.TEST_NOT_EXISTED_INSTANCE_ID, + ), + statusCode: 404, + responseBody: { + statusCode: 404, + message: 'Invalid database instance id.', + error: 'Not Found' + }, + }, + { + name: 'Should return 0 array when no history items yet', + before: async () => { + await localDb.generateNCommandExecutions({ + databaseId: constants.TEST_INSTANCE_ID, + id: constants.TEST_COMMAND_EXECUTION_ID_1, + }, 2); + }, + after: async () => { + expect(await (await localDb.getRepository(localDb.repositories.COMMAND_EXECUTION)).count({})).to.eq(0) + }, + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/ui/src/components/query-card/QueryCard.tsx b/redisinsight/ui/src/components/query-card/QueryCard.tsx index 368658f6ee..d1890eaada 100644 --- a/redisinsight/ui/src/components/query-card/QueryCard.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCard.tsx @@ -39,6 +39,7 @@ export interface Props { summary?: ResultsSummary createdAt?: Date loading?: boolean + clearing?: boolean isNotStored?: boolean executionTime?: number db?: number @@ -80,6 +81,7 @@ const QueryCard = (props: Props) => { onQueryProfile, onQueryReRun, loading, + clearing, emptyCommand, isNotStored, executionTime, @@ -173,6 +175,7 @@ const QueryCard = (props: Props) => { isFullScreen={isFullScreen} query={command} loading={loading} + clearing={clearing} createdAt={createdAt} message={message} queryType={queryType} diff --git a/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx b/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx index b111eb7c78..738db076af 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx @@ -62,6 +62,7 @@ export interface Props { queryType: WBQueryType selectedValue: string loading?: boolean + clearing?: boolean executionTime?: number emptyCommand?: boolean db?: number @@ -96,6 +97,7 @@ const QueryCardHeader = (props: Props) => { toggleFullScreen, query = '', loading, + clearing, message, createdAt, mode, @@ -169,6 +171,7 @@ const QueryCardHeader = (props: Props) => { const handleQueryDelete = (event: React.MouseEvent) => { eventStop(event) onQueryDelete() + sendEvent(TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED, query) } const handleQueryReRun = (event: React.MouseEvent) => { @@ -404,7 +407,7 @@ const QueryCardHeader = (props: Props) => { { rawMode: true, } }) + + sendEventTelemetry.mockRestore() + }) + + it('should call proper telemetry on delete', async () => { + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('delete-command')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED, + eventData: { + databaseId: 'instanceId', + command: 'info' + } + }) + + sendEventTelemetry.mockRestore() + + fireEvent.click(screen.getByTestId('clear-history-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.WORKBENCH_CLEAR_ALL_RESULTS_CLICKED, + eventData: { + databaseId: 'instanceId' + } + }) + + sendEventTelemetry.mockRestore() }) }) describe('Raw mode', () => { diff --git a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx index 314a329aad..afa9db5fc1 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react' import cx from 'classnames' -import { EuiIcon, EuiText } from '@elastic/eui' +import { EuiButtonEmpty, EuiIcon, EuiText } from '@elastic/eui' import { Theme } from 'uiSrc/constants' import { ProfileQueryType } from 'uiSrc/pages/workbench/constants' @@ -17,22 +17,26 @@ import styles from './styles.module.scss' export interface Props { items: CommandExecutionUI[] + clearing: boolean activeMode: RunQueryMode activeResultsMode?: ResultsMode scrollDivRef: React.Ref onQueryReRun: (query: string, commandId?: Nullable, executeParams?: CodeButtonParams) => void onQueryDelete: (commandId: string) => void + onAllQueriesDelete: () => void onQueryOpen: (commandId: string) => void onQueryProfile: (query: string, commandId?: Nullable, executeParams?: CodeButtonParams) => void } const WBResults = (props: Props) => { const { items = [], + clearing, activeMode, activeResultsMode, onQueryReRun, onQueryProfile, onQueryDelete, + onAllQueriesDelete, onQueryOpen, scrollDivRef } = props @@ -69,6 +73,20 @@ const WBResults = (props: Props) => { return (
+ {!!items.length && ( +
+ onAllQueriesDelete?.()} + data-testid="clear-history-btn" + > + Clear Results + +
+ )}
{items.map(( { @@ -93,6 +111,7 @@ const WBResults = (props: Props) => { isOpen={isOpen} result={result} summary={summary} + clearing={clearing} loading={loading} command={command} createdAt={createdAt} diff --git a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/styles.module.scss index d59c21a5b6..2331835ac9 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/styles.module.scss @@ -14,6 +14,14 @@ overflow: auto; } +.header { + height: 42px; + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 12px; +} + .noResults { width: 326px; height: 100%; @@ -42,3 +50,14 @@ width: 42px !important; height: 42px !important; } + +.clearAllBtn { + font-size: 14px !important; + + :global { + .euiIcon { + width: 14px !important; + height: 14px !important; + } + } +} diff --git a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx index ec91f806e5..ff52d3aa66 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx @@ -7,12 +7,14 @@ import WBResults from './WBResults' export interface Props { items: CommandExecutionUI[] + clearing: boolean activeMode: RunQueryMode activeResultsMode: ResultsMode scrollDivRef: React.Ref onQueryReRun: (query: string, commandId?: Nullable, executeParams?: CodeButtonParams) => void onQueryOpen: (commandId: string) => void onQueryDelete: (commandId: string) => void + onAllQueriesDelete: () => void onQueryProfile: (query: string, commandId?: Nullable, executeParams?: CodeButtonParams) => void } diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx index be4376674d..aea483109f 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx @@ -34,6 +34,7 @@ const verticalPanelIds = { export interface Props { script: string items: CommandExecutionUI[] + clearing: boolean setScript: (script: string) => void setScriptEl: Function scriptEl: Nullable @@ -43,6 +44,7 @@ export interface Props { onSubmit: (query?: string, commandId?: Nullable, executeParams?: CodeButtonParams) => void onQueryOpen: (commandId?: string) => void onQueryDelete: (commandId: string) => void + onAllQueriesDelete: () => void onQueryChangeMode: () => void onChangeGroupMode: () => void } @@ -61,6 +63,7 @@ const WBView = (props: Props) => { const { script = '', items, + clearing, setScript, setScriptEl, scriptEl, @@ -69,6 +72,7 @@ const WBView = (props: Props) => { onSubmit, onQueryOpen, onQueryDelete, + onAllQueriesDelete, onQueryChangeMode, onChangeGroupMode, scrollDivRef, @@ -228,6 +232,7 @@ const WBView = (props: Props) => { > { onQueryProfile={handleProfile} onQueryOpen={onQueryOpen} onQueryDelete={onQueryDelete} + onAllQueriesDelete={onAllQueriesDelete} /> diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx index da02905cca..50baf9f251 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx @@ -13,9 +13,13 @@ import { import QueryWrapper from 'uiSrc/components/query' import { Props as QueryProps } from 'uiSrc/components/query/QueryWrapper' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { loadWBHistory, sendWBCommandAction } from 'uiSrc/slices/workbench/wb-results' -import { getWBGuides } from 'uiSrc/slices/workbench/wb-guides' -import { getWBTutorials } from 'uiSrc/slices/workbench/wb-tutorials' +import { + clearWbResults, + loadWBHistory, + processWBCommand, + sendWBCommandAction, + workbenchResultsSelector +} from 'uiSrc/slices/workbench/wb-results' import { getWBCustomTutorials } from 'uiSrc/slices/workbench/wb-custom-tutorials' import WBViewWrapper from './WBViewWrapper' @@ -71,6 +75,10 @@ jest.mock('uiSrc/slices/workbench/wb-results', () => ({ sendWBCommandClusterAction: jest.fn(), processUnsupportedCommand: jest.fn(), updateCliCommandHistory: jest.fn, + workbenchResultsSelector: jest.fn().mockReturnValue({ + loading: false, + items: [] + }) })) jest.mock('uiSrc/slices/workbench/wb-guides', () => { @@ -111,6 +119,39 @@ describe('WBViewWrapper', () => { ) }) + it('should call delete command', () => { + (workbenchResultsSelector as jest.Mock).mockImplementation(() => ({ + items: [ + { id: '1' }, + ], + })) + render() + + fireEvent.click(screen.getByTestId('delete-command')) + expect(clearStoreActions(store.getActions().slice(-1))).toEqual(clearStoreActions([processWBCommand('1')])) + }) + + it('should call delete all command', () => { + (workbenchResultsSelector as jest.Mock).mockImplementation(() => ({ + items: [ + { id: '1' }, + ], + })) + render() + + fireEvent.click(screen.getByTestId('clear-history-btn')) + expect(clearStoreActions(store.getActions().slice(-1))).toEqual(clearStoreActions([clearWbResults()])) + }) + + it('should not display clear results when with empty history', () => { + (workbenchResultsSelector as jest.Mock).mockImplementation(() => ({ + items: [], + })) + render() + + expect(screen.queryByTestId('clear-history-btn')).not.toBeInTheDocument() + }) + it.skip('"onSubmit" for Cluster connection should call "sendWBCommandClusterAction"', async () => { (connectedInstanceSelector as jest.Mock).mockImplementation(() => ({ id: '123', diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx index 617f798c36..67d2238555 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx @@ -7,32 +7,33 @@ import { chunk, without } from 'lodash' import { CodeButtonParams } from 'uiSrc/pages/workbench/components/enablement-area/interfaces' import { - Nullable, - removeMonacoComments, - splitMonacoValuePerLines, - getMultiCommands, - scrollIntoView, getExecuteParams, - isGroupMode, getMonacoLines, - Maybe, - isGroupResults, + getMultiCommands, getParsedParamsInQuery, + isGroupMode, + isGroupResults, + Maybe, + Nullable, + removeMonacoComments, + scrollIntoView, + splitMonacoValuePerLines, } from 'uiSrc/utils' import { localStorageService } from 'uiSrc/services' import { - sendWBCommandAction, - workbenchResultsSelector, - fetchWBHistoryAction, + clearWbResultsAction, deleteWBCommandAction, - sendWBCommandClusterAction, - resetWBHistoryItems, fetchWBCommandAction, + fetchWBHistoryAction, + resetWBHistoryItems, + sendWBCommandAction, + sendWBCommandClusterAction, + workbenchResultsSelector, } from 'uiSrc/slices/workbench/wb-results' import { ConnectionType, ExecuteQueryParams, Instance, IPluginVisualization } from 'uiSrc/slices/interfaces' -import { initialState as instanceInitState, connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { connectedInstanceSelector, initialState as instanceInitState } from 'uiSrc/slices/instances/instances' import { ClusterNodeRole } from 'uiSrc/slices/interfaces/cli' -import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' +import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' import { cliSettingsSelector, fetchBlockingCliCommandsAction } from 'uiSrc/slices/cli/cli-settings' import { appContextWorkbench, setWorkbenchScript } from 'uiSrc/slices/app/context' import { appPluginsSelector } from 'uiSrc/slices/app/plugins' @@ -72,7 +73,7 @@ let state: IState = { const WBViewWrapper = () => { const { instanceId } = useParams<{ instanceId: string }>() - const { loading, items } = useSelector(workbenchResultsSelector) + const { loading, items, clearing } = useSelector(workbenchResultsSelector) const { unsupportedCommands, blockingCommands } = useSelector(cliSettingsSelector) const { batchSize = PIPELINE_COUNT_DEFAULT } = useSelector(userSettingsConfigSelector) ?? {} const { cleanup: cleanupWB } = useSelector(userSettingsWBSelector) @@ -256,6 +257,16 @@ const WBViewWrapper = () => { dispatch(deleteWBCommandAction(commandId)) } + const handleAllQueriesDelete = () => { + dispatch(clearWbResultsAction()) + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_CLEAR_ALL_RESULTS_CLICKED, + eventData: { + databaseId: instanceId, + } + }) + } + const handleQueryOpen = (commandId: string = '') => { dispatch(fetchWBCommandAction(commandId)) } @@ -286,6 +297,7 @@ const WBViewWrapper = () => { return ( { onSubmit={sourceValueSubmit} onQueryOpen={handleQueryOpen} onQueryDelete={handleQueryDelete} + onAllQueriesDelete={handleAllQueriesDelete} onQueryChangeMode={handleChangeQueryRunMode} resultsMode={resultsMode} onChangeGroupMode={handleChangeGroupMode} diff --git a/redisinsight/ui/src/slices/interfaces/workbench.ts b/redisinsight/ui/src/slices/interfaces/workbench.ts index a304df6540..84cf70c900 100644 --- a/redisinsight/ui/src/slices/interfaces/workbench.ts +++ b/redisinsight/ui/src/slices/interfaces/workbench.ts @@ -11,6 +11,7 @@ export interface StateWorkbenchSettings { export interface StateWorkbenchResults { loading: boolean processing: boolean + clearing: boolean error: string items: CommandExecutionUI[] } diff --git a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts index ee00aa4860..8770a6fcb7 100644 --- a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts +++ b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts @@ -34,6 +34,10 @@ import reducer, { deleteWBCommandSuccess, resetWBHistoryItems, fetchWBHistoryAction, + clearWbResultsAction, + clearWbResults, + clearWbResultsSuccess, + clearWbResultsFailed, } from '../../workbench/wb-results' jest.mock('uiSrc/services', () => ({ @@ -333,6 +337,80 @@ describe('workbench results slice', () => { }) }) + describe('clearWbResults', () => { + it('should properly set state', () => { + // Arrange + + const state = { + ...initialState, + clearing: true + } + + // Act + const nextState = reducer(initialState, clearWbResults()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + results: nextState, + }, + }) + expect(workbenchResultsSelector(rootState)).toEqual(state) + }) + }) + + describe('clearWbResultsSuccess', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialStateWithItems, + clearing: true + } + + const state = { + ...initialState, + clearing: false + } + + // Act + const nextState = reducer(currentState, clearWbResultsSuccess()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + results: nextState, + }, + }) + expect(workbenchResultsSelector(rootState)).toEqual(state) + }) + }) + + describe('clearWbResultsFailed', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialStateWithItems, + clearing: true + } + + const state = { + ...initialStateWithItems, + clearing: false + } + + // Act + const nextState = reducer(currentState, clearWbResultsFailed()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + results: nextState, + }, + }) + expect(workbenchResultsSelector(rootState)).toEqual(state) + }) + }) + describe('thunks', () => { describe('Standalone Cli commands', () => { it('call both sendWBCommandAction and sendWBCommandSuccess when response status is successed', async () => { @@ -709,6 +787,50 @@ describe('workbench results slice', () => { }) }) + describe('clearWbResultsAction', () => { + it('should call proper actions on success', async () => { + // Arrange + const responsePayload = { status: 200 } + + apiService.delete = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(clearWbResultsAction()) + + // Assert + const expectedActions = [ + clearWbResults(), + clearWbResultsSuccess() + ] + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) + }) + + it('should call proper actions on fail', async () => { + // Arrange + const errorMessage = 'Some error' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.delete = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(clearWbResultsAction()) + + // Assert + const expectedActions = [ + clearWbResults(), + addErrorNotification(responsePayload as AxiosError), + clearWbResultsFailed(), + ] + + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) + }) + }) + describe('Fetch list of commands', () => { it('call both fetchWBHistoryAction and fetchWBCommandSuccess when response status is successed', async () => { // Arrange diff --git a/redisinsight/ui/src/slices/workbench/wb-results.ts b/redisinsight/ui/src/slices/workbench/wb-results.ts index f55e1327b5..80d1bed854 100644 --- a/redisinsight/ui/src/slices/workbench/wb-results.ts +++ b/redisinsight/ui/src/slices/workbench/wb-results.ts @@ -27,6 +27,7 @@ import { export const initialState: StateWorkbenchResults = { loading: false, processing: false, + clearing: false, error: '', items: [], } @@ -174,6 +175,19 @@ const workbenchResultsSlice = createSlice({ stopProcessing: (state) => { state.processing = false + }, + + clearWbResults: (state) => { + state.clearing = true + }, + + clearWbResultsSuccess: (state) => { + state.clearing = false + state.items = [] + }, + + clearWbResultsFailed: (state) => { + state.clearing = false } }, }) @@ -193,7 +207,10 @@ export const { toggleOpenWBResult, deleteWBCommandSuccess, resetWBHistoryItems, - stopProcessing + stopProcessing, + clearWbResults, + clearWbResultsSuccess, + clearWbResultsFailed, } = workbenchResultsSlice.actions // A selector @@ -420,3 +437,36 @@ export function deleteWBCommandAction( } } } + +// Asynchronous thunk action +export function clearWbResultsAction( + onSuccessAction?: () => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + try { + const state = stateInit() + const { id = '' } = state.connections.instances.connectedInstance + + dispatch(clearWbResults()) + + const { status } = await apiService.delete( + getUrl( + id, + ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, + ), + ) + + if (isStatusSuccessful(status)) { + dispatch(clearWbResultsSuccess()) + + onSuccessAction?.() + } + } catch (_err) { + const error = _err as AxiosError + dispatch(addErrorNotification(error)) + dispatch(clearWbResultsFailed()) + onFailAction?.() + } + } +} diff --git a/redisinsight/ui/src/styles/components/_buttons.scss b/redisinsight/ui/src/styles/components/_buttons.scss index 658ec77acd..22b713fed1 100644 --- a/redisinsight/ui/src/styles/components/_buttons.scss +++ b/redisinsight/ui/src/styles/components/_buttons.scss @@ -103,6 +103,10 @@ font: normal normal 500 15px/20px Graphik, sans-serif !important; } +.euiButtonEmpty__text { + font: normal normal 400 14px/18px Graphik, sans-serif; +} + .euiButton--small { height: 30px !important; line-height: 30px !important; diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 534e871d65..643748ba42 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -121,6 +121,8 @@ export enum TelemetryEvent { WORKBENCH_ENABLEMENT_AREA_TUTORIAL_INFO_CLICKED = 'WORKBENCH_ENABLEMENT_AREA_TUTORIAL_INFO_CLICKED', WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED = 'WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_CLICKED', WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_SUBMITTED = 'WORKBENCH_ENABLEMENT_AREA_DATA_UPLOAD_SUBMITTED', + WORKBENCH_CLEAR_RESULT_CLICKED = 'WORKBENCH_CLEAR_RESULT_CLICKED', + WORKBENCH_CLEAR_ALL_RESULTS_CLICKED = 'WORKBENCH_CLEAR_ALL_RESULTS_CLICKED', PROFILER_OPENED = 'PROFILER_OPENED', PROFILER_STARTED = 'PROFILER_STARTED',