Skip to content

Commit

Permalink
feat: sort table by visible text (#1205)
Browse files Browse the repository at this point in the history
**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 <[email protected]>
  • Loading branch information
soleksy-splunk and srv-rr-github-token authored Jun 4, 2024
1 parent 3be8d48 commit 8561e0f
Show file tree
Hide file tree
Showing 9 changed files with 1,152 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,90 @@ 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,
serviceName,
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<Array<{ name: string; content: Record<string, string>; id: string }>>
) => {
const obj: RowDataType = {};

services?.forEach((service, index) => {
if (service && service.name && apiData) {
const tmpObj: Record<string, RowDataFields> = {};
apiData[index].forEach((val) => {
tmpObj[val.name] = {
...val.content,
id: val.id,
Expand All @@ -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) => {
Expand All @@ -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]: {
Expand All @@ -110,15 +146,15 @@ function TableWrapper({
})
);
const body = new URLSearchParams();
body.append('disabled', !row.disabled);
body.append('disabled', String(!row.disabled));
axiosCallWrapper({
serviceName: `${row.serviceName}/${row.name}`,
body,
customHeaders: { 'Content-Type': 'application/x-www-form-urlencoded' },
method: 'post',
handleError: true,
callbackOnError: () => {
setRowData((currentRowData) =>
setRowData((currentRowData: RowDataType) =>
update(currentRowData, {
[row.serviceName]: {
[row.name]: {
Expand All @@ -129,7 +165,7 @@ function TableWrapper({
);
},
}).then((response) => {
setRowData((currentRowData) =>
setRowData((currentRowData: RowDataType) =>
update(currentRowData, {
[row.serviceName]: {
[row.name]: {
Expand All @@ -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<string, RowDataFields>) => {
const matchedRows: Record<string, AcceptableFormValueOrNull>[] = [];
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<Record<string, AcceptableFormValueOrNull>> = [];
if (searchType === 'all') {
Object.keys(rowData).forEach((key) => {
let newArr = [];
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -238,7 +296,8 @@ function TableWrapper({
return <WaitSpinnerWrapper size="medium" />;
}

const [filteredData, totalElement, allFilteredData] = getRowData();
const { filteredData, totalElement, allFilteredData } = getRowData();

return (
<>
<TableHeader
Expand All @@ -261,7 +320,6 @@ function TableWrapper({
sortKey={sortKey}
handleOpenPageStyleDialog={handleOpenPageStyleDialog}
tableConfig={tableConfig}
services={services}
/>
</>
);
Expand Down
45 changes: 45 additions & 0 deletions ui/src/components/table/stories/TableWrapper.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TableContextProvider>
{(<TableWrapper {...props} />) as unknown as Node}
</TableContextProvider>
);
},
} satisfies Meta<ITableWrapperStoriesProps>;

export default meta;
type Story = StoryObj<typeof meta>;

export const OuathBasic: Story = {
args: {
page: 'configuration',
serviceName: 'account',
handleRequestModalOpen: fn(),
handleOpenPageStyleDialog: fn(),
displayActionBtnAllRows: false,
config: getSimpleConfig() as GlobalConfig,
},
parameters: {
msw: {
handlers: ServerHandlers,
},
},
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 8561e0f

Please sign in to comment.