From 8561e0fd5ea99daab5030bec00bb2fcac3b7c9b1 Mon Sep 17 00:00:00 2001 From: soleksy-splunk <143183665+soleksy-splunk@users.noreply.github.com> Date: Tue, 4 Jun 2024 22:39:05 +0200 Subject: [PATCH] feat: sort table by visible text (#1205) **Issue number:** [ADDON-59778](https://splunk.atlassian.net/browse/ADDON-59778) ## Summary [UCC UI] Add support for sorting functionality when custom mapping is used in global config file ### Changes Sorting functionality works on mapped text not on keys. ### User experience Right now if user will sort by mapped property it will be displayed correctly. ## Checklist If your change doesn't seem to apply, please leave them unchecked. * [x] I have performed a self-review of this change * [x] Changes have been tested * [ ] Changes are documented * [x] PR title follows [conventional commit semantics](https://www.conventionalcommits.org/en/v1.0.0/) --------- Co-authored-by: srv-rr-github-token <94607705+srv-rr-github-token@users.noreply.github.com> --- .../{TableWrapper.jsx => TableWrapper.tsx} | 180 +++++--- .../table/stories/TableWrapper.stories.tsx | 45 ++ .../TableWrapper-ouath-basic-chromium.png | 3 + .../components/table/stories/configMockups.ts | 244 +++++++++++ .../components/table/stories/rowDataMockup.ts | 410 ++++++++++++++++++ ui/src/components/table/tests/MockedData.ts | 166 +++++++ .../table/tests/TableWrapper.test.tsx | 152 +++++++ ui/src/context/TableContext.tsx | 13 +- ui/src/types/components/shareableTypes.ts | 2 + 9 files changed, 1152 insertions(+), 63 deletions(-) rename ui/src/components/table/{TableWrapper.jsx => TableWrapper.tsx} (57%) create mode 100644 ui/src/components/table/stories/TableWrapper.stories.tsx create mode 100644 ui/src/components/table/stories/__images__/TableWrapper-ouath-basic-chromium.png create mode 100644 ui/src/components/table/stories/configMockups.ts create mode 100644 ui/src/components/table/stories/rowDataMockup.ts create mode 100644 ui/src/components/table/tests/MockedData.ts create mode 100644 ui/src/components/table/tests/TableWrapper.test.tsx diff --git a/ui/src/components/table/TableWrapper.jsx b/ui/src/components/table/TableWrapper.tsx similarity index 57% rename from ui/src/components/table/TableWrapper.jsx rename to ui/src/components/table/TableWrapper.tsx index 482680ad0..51cfd3748 100644 --- a/ui/src/components/table/TableWrapper.jsx +++ b/ui/src/components/table/TableWrapper.tsx @@ -2,15 +2,58 @@ import React, { useState, useContext, useEffect, memo } from 'react'; import update from 'immutability-helper'; import axios from 'axios'; import PropTypes from 'prop-types'; +import { HeadCellSortHandler } from '@splunk/react-ui/Table'; import { WaitSpinnerWrapper } from './CustomTableStyle'; import { axiosCallWrapper } from '../../util/axiosCallWrapper'; import { getUnifiedConfigs, generateToast, isTrue } from '../../util/util'; import CustomTable from './CustomTable'; import TableHeader from './TableHeader'; -import TableContext from '../../context/TableContext'; +import TableContext, { RowDataType, RowDataFields } from '../../context/TableContext'; import { PAGE_INPUT } from '../../constants/pages'; import { parseErrorMsg } from '../../util/messageUtil'; +import { Mode } from '../../constants/modes'; +import { GlobalConfig } from '../../types/globalConfig/globalConfig'; +import { AcceptableFormValueOrNull } from '../../types/components/shareableTypes'; + +export interface ITableWrapperProps { + page: string; + serviceName: string; + handleRequestModalOpen: () => void; + handleOpenPageStyleDialog: (row: IRowData, mode: Mode) => void; + displayActionBtnAllRows: boolean; +} + +interface IRowData {} + +const getTableConfigAndServices = ( + page: string, + unifiedConfigs: GlobalConfig, + serviceName: string +) => { + const services = + page === PAGE_INPUT + ? unifiedConfigs.pages.inputs?.services + : unifiedConfigs.pages.configuration.tabs.filter((x) => x.name === serviceName); + + if (page === PAGE_INPUT) { + if (unifiedConfigs.pages.inputs && 'table' in unifiedConfigs.pages.inputs) { + return { services, tableConfig: unifiedConfigs.pages.inputs.table }; + } + + const serviceWithTable = services?.find((x) => x.name === serviceName); + const tableData = serviceWithTable && 'table' in serviceWithTable && serviceWithTable.table; + + return { services, tableConfig: tableData || {} }; + } + + const tableConfig = + unifiedConfigs.pages.configuration.tabs.find((x) => x.name === serviceName)?.table || {}; + return { + services, + tableConfig, + }; +}; function TableWrapper({ page, @@ -18,38 +61,31 @@ function TableWrapper({ handleRequestModalOpen, handleOpenPageStyleDialog, displayActionBtnAllRows, -}) { +}: ITableWrapperProps) { const [sortKey, setSortKey] = useState('name'); const [sortDir, setSortDir] = useState('asc'); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const { rowData, setRowData, pageSize, currentPage, searchText, searchType } = - useContext(TableContext); + useContext(TableContext)!; const unifiedConfigs = getUnifiedConfigs(); + const { services, tableConfig } = getTableConfigAndServices(page, unifiedConfigs, serviceName); - const services = - page === PAGE_INPUT - ? unifiedConfigs.pages.inputs.services - : unifiedConfigs.pages.configuration.tabs.filter((x) => x.name === serviceName); - - const tableConfig = - page === PAGE_INPUT - ? unifiedConfigs.pages.inputs.table || - services.find((x) => x.name === serviceName).table - : unifiedConfigs.pages.configuration.tabs.find((x) => x.name === serviceName).table; - - const { moreInfo } = tableConfig; - const headers = tableConfig.header; + const moreInfo = tableConfig && 'moreInfo' in tableConfig ? tableConfig?.moreInfo : null; + const headers = tableConfig && 'header' in tableConfig ? tableConfig?.header : null; const isTabs = !!serviceName; - const modifyAPIResponse = (data) => { - const obj = {}; - services.forEach((service, index) => { - if (service && service.name && data) { - const tmpObj = {}; - data[index].forEach((val) => { + const modifyAPIResponse = ( + apiData: Array; id: string }>> + ) => { + const obj: RowDataType = {}; + + services?.forEach((service, index) => { + if (service && service.name && apiData) { + const tmpObj: Record = {}; + apiData[index].forEach((val) => { tmpObj[val.name] = { ...val.content, id: val.id, @@ -61,20 +97,20 @@ function TableWrapper({ obj[service.name] = tmpObj; } }); + setRowData(obj); setLoading(false); }; const fetchInputs = () => { - const requests = []; - services.forEach((service) => { - requests.push( + const requests = + services?.map((service) => axiosCallWrapper({ serviceName: service.name, params: { count: -1 }, }) - ); - }); + ) || []; + axios .all(requests) .catch((caughtError) => { @@ -99,8 +135,8 @@ function TableWrapper({ * * @param row {Object} row */ - const changeToggleStatus = (row) => { - setRowData((currentRowData) => + const changeToggleStatus = (row: RowDataFields) => { + setRowData((currentRowData: RowDataType) => update(currentRowData, { [row.serviceName]: { [row.name]: { @@ -110,7 +146,7 @@ function TableWrapper({ }) ); const body = new URLSearchParams(); - body.append('disabled', !row.disabled); + body.append('disabled', String(!row.disabled)); axiosCallWrapper({ serviceName: `${row.serviceName}/${row.name}`, body, @@ -118,7 +154,7 @@ function TableWrapper({ method: 'post', handleError: true, callbackOnError: () => { - setRowData((currentRowData) => + setRowData((currentRowData: RowDataType) => update(currentRowData, { [row.serviceName]: { [row.name]: { @@ -129,7 +165,7 @@ function TableWrapper({ ); }, }).then((response) => { - setRowData((currentRowData) => + setRowData((currentRowData: RowDataType) => update(currentRowData, { [row.serviceName]: { [row.name]: { @@ -143,50 +179,53 @@ function TableWrapper({ }); }; - const handleSort = (e, val) => { + const handleSort: HeadCellSortHandler = (e, val) => { const prevSortKey = sortKey; const prevSortDir = prevSortKey === val.sortKey ? sortDir : 'none'; const nextSortDir = prevSortDir === 'asc' ? 'desc' : 'asc'; setSortDir(nextSortDir); - setSortKey(val.sortKey); + if (val.sortKey) { + setSortKey(val.sortKey); + } }; /** * - * @param {Array} data + * @param {Array} serviceData data for single service * This function will iterate an arrray and match each key-value with the searchText * It will return a new array which will match with searchText */ - const findByMatchingValue = (data) => { - const arr = []; - const tableFields = []; + const findByMatchingValue = (serviceData: Record) => { + const matchedRows: Record[] = []; + const searchableFields: string[] = []; - headers.forEach((headData) => { - tableFields.push(headData.field); + headers?.forEach((headData: { field: string }) => { + searchableFields.push(headData.field); }); - moreInfo?.forEach((moreInfoData) => { - tableFields.push(moreInfoData.field); + moreInfo?.forEach((moreInfoData: { field: string }) => { + searchableFields.push(moreInfoData.field); }); - Object.keys(data).forEach((v) => { + Object.keys(serviceData).forEach((v) => { let found = false; - Object.keys(data[v]).forEach((vv) => { + Object.keys(serviceData[v]).forEach((vv) => { + const formValue = serviceData[v][vv]; if ( - tableFields.includes(vv) && - typeof data[v][vv] === 'string' && - data[v][vv].toLowerCase().includes(searchText.toLowerCase().trim()) && + searchableFields.includes(vv) && + typeof formValue === 'string' && + formValue.toLowerCase().includes(searchText.toLowerCase().trim()) && !found ) { - arr.push(data[v]); + matchedRows.push(serviceData[v]); found = true; } }); }); - return arr; + return matchedRows; }; const getRowData = () => { - let arr = []; + let allRowsData: Array> = []; if (searchType === 'all') { Object.keys(rowData).forEach((key) => { let newArr = []; @@ -195,27 +234,42 @@ function TableWrapper({ } else { newArr = Object.keys(rowData[key]).map((val) => rowData[key][val]); } - arr = arr.concat(newArr); + allRowsData = allRowsData.concat(newArr); }); } else { - arr = findByMatchingValue(rowData[searchType]); + allRowsData = findByMatchingValue(rowData[searchType]); } // For Inputs page, filter the data when tab change if (isTabs) { - arr = arr.filter((v) => v.serviceName === serviceName); + allRowsData = allRowsData.filter((v) => v.serviceName === serviceName); } + const headerMapping = + headers?.find((header: { field: string }) => header.field === sortKey)?.mapping || {}; // Sort the array based on the sort value - const sortedArr = arr.sort((rowA, rowB) => { + const sortedArr = allRowsData.sort((rowA, rowB) => { if (sortDir === 'asc') { - const rowAValue = rowA[sortKey] === undefined ? '' : rowA[sortKey]; - const rowBValue = rowB[sortKey] === undefined ? '' : rowB[sortKey]; + const rowAValue = + rowA[sortKey] === undefined + ? '' + : headerMapping[String(rowA[sortKey])] || rowA[sortKey]; + const rowBValue = + rowB[sortKey] === undefined + ? '' + : headerMapping[String(rowB[sortKey])] || rowB[sortKey]; return rowAValue > rowBValue ? 1 : -1; } if (sortDir === 'desc') { - const rowAValue = rowA[sortKey] === undefined ? '' : rowA[sortKey]; - const rowBValue = rowB[sortKey] === undefined ? '' : rowB[sortKey]; + const rowAValue = + rowA[sortKey] === undefined + ? '' + : headerMapping[String(rowA[sortKey])] || rowA[sortKey]; + const rowBValue = + rowB[sortKey] === undefined + ? '' + : headerMapping[String(rowB[sortKey])] || rowB[sortKey]; + return rowBValue > rowAValue ? 1 : -1; } return 0; @@ -227,7 +281,11 @@ function TableWrapper({ updatedArr = sortedArr.slice((currentPage - 1) * pageSize, pageSize); } - return [updatedArr, arr.length, arr]; + return { + filteredData: updatedArr, + totalElement: allRowsData.length, + allFilteredData: allRowsData, + }; }; if (error) { @@ -238,7 +296,8 @@ function TableWrapper({ return ; } - const [filteredData, totalElement, allFilteredData] = getRowData(); + const { filteredData, totalElement, allFilteredData } = getRowData(); + return ( <> ); diff --git a/ui/src/components/table/stories/TableWrapper.stories.tsx b/ui/src/components/table/stories/TableWrapper.stories.tsx new file mode 100644 index 000000000..ca637108d --- /dev/null +++ b/ui/src/components/table/stories/TableWrapper.stories.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { setUnifiedConfig } from '../../../util/util'; +import { GlobalConfig } from '../../../types/globalConfig/globalConfig'; +import TableWrapper, { ITableWrapperProps } from '../TableWrapper'; +import { getSimpleConfig } from './configMockups'; +import { TableContextProvider } from '../../../context/TableContext'; +import { ServerHandlers } from './rowDataMockup'; + +interface ITableWrapperStoriesProps extends ITableWrapperProps { + config: GlobalConfig; +} + +const meta = { + title: 'TableWrapper', + render: (props) => { + setUnifiedConfig(props.config); + return ( + + {() as unknown as Node} + + ); + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const OuathBasic: Story = { + args: { + page: 'configuration', + serviceName: 'account', + handleRequestModalOpen: fn(), + handleOpenPageStyleDialog: fn(), + displayActionBtnAllRows: false, + config: getSimpleConfig() as GlobalConfig, + }, + parameters: { + msw: { + handlers: ServerHandlers, + }, + }, +}; diff --git a/ui/src/components/table/stories/__images__/TableWrapper-ouath-basic-chromium.png b/ui/src/components/table/stories/__images__/TableWrapper-ouath-basic-chromium.png new file mode 100644 index 000000000..c36b411ac --- /dev/null +++ b/ui/src/components/table/stories/__images__/TableWrapper-ouath-basic-chromium.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:286299445b0d7362c6ff99aad3ef3ce0913cb21dc964e662c7bfe03315b42128 +size 23671 diff --git a/ui/src/components/table/stories/configMockups.ts b/ui/src/components/table/stories/configMockups.ts new file mode 100644 index 000000000..ec7d910aa --- /dev/null +++ b/ui/src/components/table/stories/configMockups.ts @@ -0,0 +1,244 @@ +import { GlobalConfig } from '../../../types/globalConfig/globalConfig'; + +export const SIMPLE_NAME_TABLE_MOCK_DATA = { + pages: { + configuration: { + tabs: [ + { + name: 'account', + table: { + actions: ['edit', 'delete', 'clone'], + header: [ + { + label: 'Name', + field: 'name', + }, + ], + }, + entity: [ + { + type: 'text', + label: 'Name', + validators: [ + { + type: 'string', + errorMsg: 'Length of ID should be between 1 and 50', + minLength: 1, + maxLength: 50, + }, + { + type: 'regex', + errorMsg: + 'Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.', + pattern: '^[a-zA-Z]\\w*$', + }, + ], + field: 'name', + help: 'Enter a unique name for this account.', + required: true, + }, + ], + title: 'Account', + restHandlerModule: 'splunk_ta_uccexample_validate_account_rh', + restHandlerClass: 'CustomAccountValidator', + }, + ], + title: 'Configuration', + description: 'Set up your add-on', + }, + inputs: { + services: [ + { + name: 'example_input_one', + entity: [ + { + type: 'text', + label: 'Name', + validators: [ + { + type: 'regex', + errorMsg: + 'Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.', + pattern: '^[a-zA-Z]\\w*$', + }, + { + type: 'string', + errorMsg: 'Length of input name should be between 1 and 100', + minLength: 1, + maxLength: 100, + }, + ], + field: 'name', + help: 'A unique name for the data input.', + required: true, + }, + { + type: 'checkbox', + label: 'Example Checkbox', + field: 'input_one_checkbox', + help: 'This is an example checkbox for the input one entity', + defaultValue: true, + }, + ], + title: 'Example Input One', + }, + ], + title: 'Inputs', + description: 'Manage your data inputs', + table: { + actions: ['edit', 'enable', 'delete', 'search', 'clone'], + header: [ + { + label: 'Name', + field: 'name', + }, + ], + moreInfo: [ + { + label: 'Name', + field: 'name', + }, + ], + }, + }, + }, + meta: { + name: 'Splunk_TA_UCCExample', + restRoot: 'splunk_ta_uccexample', + version: '5.41.0R9c5fbfe0', + displayName: 'Splunk UCC test Add-on', + schemaVersion: '0.0.3', + }, +} satisfies GlobalConfig; + +export const getSimpleConfig = () => { + const configCp = JSON.parse(JSON.stringify(SIMPLE_NAME_TABLE_MOCK_DATA)); + return configCp; +}; + +export const TABLE_CONFIG_WITH_MAPPING = { + pages: { + configuration: { + tabs: [ + { + name: 'account', + table: { + actions: ['edit', 'delete', 'clone'], + header: [ + { + label: 'Name', + field: 'name', + }, + { + field: 'custom_text', + label: 'Custom Text', + mapping: { + a: 'wxyz=a', + ab: 'xyz=ab', + abc: 'yz=abc', + abcd: 'z=abcd', + }, + }, + ], + }, + entity: [ + { + type: 'text', + label: 'Name', + validators: [ + { + type: 'string', + errorMsg: 'Length of ID should be between 1 and 50', + minLength: 1, + maxLength: 50, + }, + { + type: 'regex', + errorMsg: + 'Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.', + pattern: '^[a-zA-Z]\\w*$', + }, + ], + field: 'name', + help: 'Enter a unique name for this account.', + required: true, + }, + { + type: 'text', + label: 'Custom Text', + field: 'custom_text', + help: 'custom text.', + }, + ], + title: 'Account', + restHandlerModule: 'splunk_ta_uccexample_validate_account_rh', + restHandlerClass: 'CustomAccountValidator', + }, + ], + title: 'Configuration', + description: 'Set up your add-on', + }, + inputs: { + services: [ + { + name: 'example_input_one', + entity: [ + { + type: 'text', + label: 'Name', + validators: [ + { + type: 'regex', + errorMsg: + 'Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.', + pattern: '^[a-zA-Z]\\w*$', + }, + { + type: 'string', + errorMsg: 'Length of input name should be between 1 and 100', + minLength: 1, + maxLength: 100, + }, + ], + field: 'name', + help: 'A unique name for the data input.', + required: true, + }, + { + type: 'checkbox', + label: 'Example Checkbox', + field: 'input_one_checkbox', + help: 'This is an example checkbox for the input one entity', + defaultValue: true, + }, + ], + title: 'Example Input One', + }, + ], + title: 'Inputs', + description: 'Manage your data inputs', + table: { + actions: ['edit', 'enable', 'delete', 'search', 'clone'], + header: [ + { + label: 'Name', + field: 'name', + }, + ], + moreInfo: [ + { + label: 'Name', + field: 'name', + }, + ], + }, + }, + }, + meta: { + name: 'Splunk_TA_UCCExample', + restRoot: 'splunk_ta_uccexample', + version: '5.41.0R9c5fbfe0', + displayName: 'Splunk UCC test Add-on', + schemaVersion: '0.0.3', + }, +} satisfies GlobalConfig; diff --git a/ui/src/components/table/stories/rowDataMockup.ts b/ui/src/components/table/stories/rowDataMockup.ts new file mode 100644 index 000000000..0a398aa9d --- /dev/null +++ b/ui/src/components/table/stories/rowDataMockup.ts @@ -0,0 +1,410 @@ +import { HttpResponse, http } from 'msw'; + +export const ROW_DATA = [ + { + name: 'aaaaaa', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaa', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: + '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaa', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaa', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaa', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaa', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'a', + token: '******', + username: 'aaaaaa', + }, + }, + { + name: 'aaaaaaa', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaaa', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: + '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaaa', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaaa', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaaa', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaaa', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'ab', + token: '******', + username: 'aaaaaaa', + }, + }, + { + name: 'bbbb', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/bbbb', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/bbbb', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/bbbb', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/bbbb', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/bbbb', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'abc', + token: '******', + username: 'aaaaaa', + }, + }, + { + name: 'ccc', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ccc', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ccc', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ccc', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ccc', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ccc', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'abcd', + token: '******', + username: 'ccc', + }, + }, + { + name: 'ddddd', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ddddd', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ddddd', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ddddd', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ddddd', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ddddd', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'ab', + token: '******', + username: 'ddddd', + }, + }, + { + name: 'test1', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test1', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test1', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test1', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test1', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test1', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'aaaaa', + token: '******', + username: 'test1', + }, + }, + { + name: 'test2', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test2', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test2', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test2', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test2', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test2', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'two', + token: '******', + username: 'test1', + }, + }, + { + name: 'testsomethingelse', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/testsomethingelse', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: + '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/testsomethingelse', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/testsomethingelse', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/testsomethingelse', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/testsomethingelse', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: 'testsomethingelse', + token: '******', + username: 'test1', + }, + }, + { + name: 'zzzzzzz', + id: 'https://localhost:8000/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/zzzzzzz', + updated: '1970-01-01T00:00:00+00:00', + links: { + alternate: + '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/zzzzzzz', + list: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/zzzzzzz', + edit: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/zzzzzzz', + remove: '/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/zzzzzzz', + }, + author: 'admin', + acl: { + app: 'Splunk_TA_UCCExample', + can_change_perms: true, + can_list: true, + can_share_app: true, + can_share_global: true, + can_share_user: true, + can_write: true, + modifiable: true, + owner: 'admin', + perms: { + read: ['*'], + write: ['admin', 'sc_admin'], + }, + removable: true, + sharing: 'global', + }, + content: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + custom_text: '222222', + token: '******', + username: 'zzzzz', + }, + }, +]; + +export const MockRowData = { + links: { + create: `/servicesNS/nobody/-/splunk_ta_uccexample_account/_new`, + }, + updated: '2023-08-21T11:54:12+00:00', + entry: ROW_DATA, + messages: [], +}; + +export const ServerHandlers = [ + http.get(`/servicesNS/nobody/-/splunk_ta_uccexample_account`, () => + HttpResponse.json(MockRowData) + ), +]; diff --git a/ui/src/components/table/tests/MockedData.ts b/ui/src/components/table/tests/MockedData.ts new file mode 100644 index 000000000..7e30d717e --- /dev/null +++ b/ui/src/components/table/tests/MockedData.ts @@ -0,0 +1,166 @@ +export const MOCKED_ROW_DATA = { + account: { + aaaaaa: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: 'aaa', + token: '******', + username: 'aaaaaa', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaa', + name: 'aaaaaa', + serviceName: 'account', + serviceTitle: 'Account', + }, + aaaaaaa: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: 'aaaaaaa', + token: '******', + username: 'aaaaaaa', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/aaaaaaa', + name: 'aaaaaaa', + serviceName: 'account', + serviceTitle: 'Account', + }, + bbbb: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: 'bbb', + token: '******', + username: 'aaaaaa', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/bbbb', + name: 'bbbb', + serviceName: 'account', + serviceTitle: 'Account', + }, + ccc: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: 'ccc', + token: '******', + username: 'ccc', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ccc', + name: 'ccc', + serviceName: 'account', + serviceTitle: 'Account', + }, + ddddd: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: 'ddd', + token: '******', + username: 'ddddd', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/ddddd', + name: 'ddddd', + serviceName: 'account', + serviceTitle: 'Account', + }, + test1: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: '1', + token: '******', + username: 'test1', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test1', + name: 'test1', + serviceName: 'account', + serviceTitle: 'Account', + }, + test2: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: '2', + token: '******', + username: 'test1', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/test2', + name: 'test2', + serviceName: 'account', + serviceTitle: 'Account', + }, + testsomethingelse: { + account_multiple_select: 'two', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: 'testsomethingelse', + token: '******', + username: 'test1', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/testsomethingelse', + name: 'testsomethingelse', + serviceName: 'account', + serviceTitle: 'Account', + }, + zzzzzzz: { + account_multiple_select: 'one', + account_radio: '1', + auth_type: 'basic', + custom_endpoint: 'login.example.com', + disabled: false, + 'eai:acl': null, + 'eai:appName': 'Splunk_TA_UCCExample', + 'eai:userName': 'nobody', + password: '******', + some_text: 'zzzzz', + token: '******', + username: 'zzzzz', + id: 'https://localhost:8080/servicesNS/nobody/Splunk_TA_UCCExample/splunk_ta_uccexample_account/zzzzzzz', + name: 'zzzzzzz', + serviceName: 'account', + serviceTitle: 'Account', + }, + }, +}; diff --git a/ui/src/components/table/tests/TableWrapper.test.tsx b/ui/src/components/table/tests/TableWrapper.test.tsx new file mode 100644 index 000000000..17bbe5c28 --- /dev/null +++ b/ui/src/components/table/tests/TableWrapper.test.tsx @@ -0,0 +1,152 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { http, HttpResponse } from 'msw'; +import { MockRowData } from '../stories/rowDataMockup'; +import TableWrapper from '../TableWrapper'; +import { server } from '../../../mocks/server'; +import { TableContextProvider } from '../../../context/TableContext'; +import { setUnifiedConfig } from '../../../util/util'; +import { SIMPLE_NAME_TABLE_MOCK_DATA, TABLE_CONFIG_WITH_MAPPING } from '../stories/configMockups'; + +jest.mock('immutability-helper'); + +const handleRequestModalOpen = jest.fn(); +const handleOpenPageStyleDialog = jest.fn(); + +it('correct render table with all elements', async () => { + const props = { + page: 'configuration', + serviceName: 'account', + handleRequestModalOpen, + handleOpenPageStyleDialog, + displayActionBtnAllRows: false, + }; + + server.use( + http.get('/servicesNS/nobody/-/splunk_ta_uccexample_account', () => + HttpResponse.json(MockRowData) + ) + ); + + setUnifiedConfig(SIMPLE_NAME_TABLE_MOCK_DATA); + + render( + + + {() as unknown as Node} + + + ); + + const numberOfItems = await screen.findByText('9 Items'); + expect(numberOfItems).toBeInTheDocument(); + + const headerNames = ['Name', 'Actions']; + + const tableHeader = document.querySelectorAll('th'); + + expect(tableHeader.length).toEqual(headerNames.length); + + headerNames.forEach((name) => { + const thWithName = Array.from(tableHeader).find( + (thElem: HTMLElement) => thElem.dataset.testLabel === name + ); + expect(thWithName).toBeTruthy(); + }); + + const currentTab = SIMPLE_NAME_TABLE_MOCK_DATA.pages.configuration.tabs.find( + (tab) => tab.name === props.serviceName + ); + + currentTab?.entity.forEach((confEntity) => + expect(screen.getByText(confEntity.label)).toBeInTheDocument() + ); +}); + +it('sort items after filtering', async () => { + const props = { + page: 'configuration', + serviceName: 'account', + handleRequestModalOpen, + handleOpenPageStyleDialog, + displayActionBtnAllRows: false, + }; + + server.use( + http.get('/servicesNS/nobody/-/splunk_ta_uccexample_account', () => + HttpResponse.json(MockRowData) + ) + ); + + setUnifiedConfig(TABLE_CONFIG_WITH_MAPPING); + + render( + + + {() as unknown as Node} + + + ); + + const numberOfItems = await screen.findByText('Custom Text'); + expect(numberOfItems).toBeInTheDocument(); + + const customHeader = document.querySelector('[data-test-label="Custom Text"]'); + expect(customHeader).toBeInTheDocument(); + + const defaultOrder = document.querySelectorAll('[data-column="custom_text"]'); + const mappedTextDefaultOrder = Array.from(defaultOrder).map((el: Node) => el.textContent); + expect(mappedTextDefaultOrder).toMatchInlineSnapshot(` + [ + "wxyz=a", + "xyz=ab", + "yz=abc", + "z=abcd", + "xyz=ab", + "aaaaa", + "two", + "testsomethingelse", + "222222", + ] + `); + + await userEvent.click(customHeader!); + + const allCustomTextsAsc = document.querySelectorAll('[data-column="custom_text"]'); + const mappedTextAsc = Array.from(allCustomTextsAsc).map((el: Node) => el.textContent); + + expect(mappedTextAsc).toMatchInlineSnapshot(` + [ + "222222", + "aaaaa", + "testsomethingelse", + "two", + "wxyz=a", + "xyz=ab", + "xyz=ab", + "yz=abc", + "z=abcd", + ] + `); + + await userEvent.click(customHeader!); + + const allCustomTextsDesc = document.querySelectorAll('[data-column="custom_text"]'); + const mappedTextDesc = Array.from(allCustomTextsDesc).map((el: Node) => el.textContent); + + expect(mappedTextDesc).toMatchInlineSnapshot(` + [ + "z=abcd", + "yz=abc", + "xyz=ab", + "xyz=ab", + "wxyz=a", + "two", + "testsomethingelse", + "aaaaa", + "222222", + ] + `); +}); diff --git a/ui/src/context/TableContext.tsx b/ui/src/context/TableContext.tsx index 926fff41b..f1987cc01 100644 --- a/ui/src/context/TableContext.tsx +++ b/ui/src/context/TableContext.tsx @@ -1,9 +1,17 @@ import React, { createContext, useState } from 'react'; import PropTypes from 'prop-types'; -import { AcceptableFormValueOrNull } from '../types/components/shareableTypes'; +import { AcceptableFormRecord } from '../types/components/shareableTypes'; + +export type RowDataFields = { + name: string; + serviceName: string; + disabled?: boolean; + id?: string; + index?: string; +} & AcceptableFormRecord; // serviceName > specificRowName > dataForRow -type RowDataType = Record>>; +export type RowDataType = Record>; export type TableContextProviderType = { rowData: RowDataType; @@ -26,6 +34,7 @@ export function TableContextProvider({ children }: { children: Node | Node[] }) const [searchType, setSearchType] = useState('all'); const [pageSize, setPageSize] = useState(10); const [currentPage, setCurrentPage] = useState(0); + return ( ;