import { queryToObject, setUrlParams } from '~/lib/utils/url_utility'; import { filterToQueryObject, processFilters, urlQueryToFilter, prepareTokens, } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { parseBoolean } from '~/lib/utils/common_utils'; import { PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG, PARAM_KEY_SEARCH, PARAM_KEY_SORT, PARAM_KEY_PAGE, PARAM_KEY_AFTER, PARAM_KEY_BEFORE, DEFAULT_SORT, RUNNER_PAGE_SIZE, STATUS_NEVER_CONTACTED, } from './constants'; import { getPaginationVariables } from './utils'; /** * The filters and sorting of the runners are built around * an object called "search" that contains the current state * of search in the UI. For example: * * ``` * const search = { * // The current tab * runnerType: 'INSTANCE_TYPE', * * // Filters in the search bar * filters: [ * { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, * { type: 'filtered-search-term', value: { data: '' } }, * ], * * // Current sorting value * sort: 'CREATED_DESC', * * // Pagination information * pagination: { page: 1 }, * }; * ``` * * An object in this format can be used to generate URLs * with the search parameters or by runner components * a input using a v-model. * * @module runner_search_utils */ /** * Validates a search value * @param {Object} search * @returns {boolean} True if the value follows the search format. */ export const searchValidator = ({ runnerType, filters, sort }) => { return ( (runnerType === null || typeof runnerType === 'string') && Array.isArray(filters) && typeof sort === 'string' ); }; const getPaginationFromParams = (params) => { const page = parseInt(params[PARAM_KEY_PAGE], 10); const after = params[PARAM_KEY_AFTER]; const before = params[PARAM_KEY_BEFORE]; if (page && (before || after)) { return { page, before, after, }; } return { page: 1, }; }; // Outdated URL parameters const STATUS_NOT_CONNECTED = 'NOT_CONNECTED'; const STATUS_ACTIVE = 'ACTIVE'; const STATUS_PAUSED = 'PAUSED'; /** * Replaces params into a URL * * @param {String} url - Original URL * @param {Object} params - Query parameters to update * @returns Updated URL */ const updateUrlParams = (url, params = {}) => { return setUrlParams(params, url, false, true, true); }; /** * Returns an updated URL for old (or deprecated) admin runner URLs. * * Use for redirecting users to currently used URLs. * * @param {String?} URL * @returns Updated URL if outdated, `null` otherwise */ export const updateOutdatedUrl = (url = window.location.href) => { const urlObj = new URL(url); const query = urlObj.search; const params = queryToObject(query, { gatherArrays: true }); const status = params[PARAM_KEY_STATUS]?.[0] || null; switch (status) { case STATUS_NOT_CONNECTED: return updateUrlParams(url, { [PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED], }); case STATUS_ACTIVE: return updateUrlParams(url, { [PARAM_KEY_PAUSED]: ['false'], [PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop! }); case STATUS_PAUSED: return updateUrlParams(url, { [PARAM_KEY_PAUSED]: ['true'], [PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop! }); default: return null; } }; /** * Takes a URL query and transforms it into a "search" object * @param {String?} query * @returns {Object} A search object */ export const fromUrlQueryToSearch = (query = window.location.search) => { const params = queryToObject(query, { gatherArrays: true }); const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null; return { runnerType, filters: prepareTokens( urlQueryToFilter(query, { filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG], filteredSearchTermKey: PARAM_KEY_SEARCH, }), ), sort: params[PARAM_KEY_SORT] || DEFAULT_SORT, pagination: getPaginationFromParams(params), }; }; /** * Takes a "search" object and transforms it into a URL. * * @param {Object} search * @param {String} url * @returns {String} New URL for the page */ export const fromSearchToUrl = ( { runnerType = null, filters = [], sort = null, pagination = {} }, url = window.location.href, ) => { const filterParams = { // Defaults [PARAM_KEY_STATUS]: [], [PARAM_KEY_RUNNER_TYPE]: [], [PARAM_KEY_TAG]: [], // Current filters ...filterToQueryObject(processFilters(filters), { filteredSearchTermKey: PARAM_KEY_SEARCH, }), }; if (runnerType) { filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType]; } if (!filterParams[PARAM_KEY_SEARCH]) { filterParams[PARAM_KEY_SEARCH] = null; } const isDefaultSort = sort !== DEFAULT_SORT; const isFirstPage = pagination?.page === 1; const otherParams = { // Sorting & Pagination [PARAM_KEY_SORT]: isDefaultSort ? sort : null, [PARAM_KEY_PAGE]: isFirstPage ? null : pagination.page, [PARAM_KEY_BEFORE]: isFirstPage ? null : pagination.before, [PARAM_KEY_AFTER]: isFirstPage ? null : pagination.after, }; return setUrlParams({ ...filterParams, ...otherParams }, url, false, true, true); }; /** * Takes a "search" object and transforms it into variables for runner a GraphQL query. * * @param {Object} search * @returns {Object} Hash of filter values */ export const fromSearchToVariables = ({ runnerType = null, filters = [], sort = null, pagination = {}, } = {}) => { const filterVariables = {}; const queryObj = filterToQueryObject(processFilters(filters), { filteredSearchTermKey: PARAM_KEY_SEARCH, }); [filterVariables.status] = queryObj[PARAM_KEY_STATUS] || []; filterVariables.search = queryObj[PARAM_KEY_SEARCH]; filterVariables.tagList = queryObj[PARAM_KEY_TAG]; if (queryObj[PARAM_KEY_PAUSED]) { filterVariables.paused = parseBoolean(queryObj[PARAM_KEY_PAUSED]); } else { filterVariables.paused = undefined; } if (runnerType) { filterVariables.type = runnerType; } if (sort) { filterVariables.sort = sort; } const paginationVariables = getPaginationVariables(pagination, RUNNER_PAGE_SIZE); return { ...filterVariables, ...paginationVariables, }; };