import { isEmpty } from 'lodash'; 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_MEMBERSHIP, PARAM_KEY_SORT, PARAM_KEY_AFTER, PARAM_KEY_BEFORE, DEFAULT_SORT, DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE, } 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: { "after": "..." }, * }; * ``` * * 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, membership, filters, sort }) => { return ( (runnerType === null || typeof runnerType === 'string') && (membership === null || typeof membership === 'string') && Array.isArray(filters) && typeof sort === 'string' ); }; const getPaginationFromParams = (params) => { return { after: params[PARAM_KEY_AFTER], before: params[PARAM_KEY_BEFORE], }; }; // Outdated URL parameters const STATUS_ACTIVE = 'ACTIVE'; const STATUS_PAUSED = 'PAUSED'; const PARAM_KEY_PAGE = 'page'; /** * 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); }; const outdatedStatusParams = (status) => { if (status === STATUS_ACTIVE) { return { [PARAM_KEY_PAUSED]: ['false'], [PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop! }; } else if (status === STATUS_PAUSED) { return { [PARAM_KEY_PAUSED]: ['true'], [PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop! }; } return {}; }; /** * 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 }); // Remove `page` completely, not needed for keyset pagination const pageParams = PARAM_KEY_PAGE in params ? { [PARAM_KEY_PAGE]: null } : {}; const status = params[PARAM_KEY_STATUS]?.[0]; const redirectParams = { // Replace paused status (active, paused) with a paused flag ...outdatedStatusParams(status), ...pageParams, }; if (!isEmpty(redirectParams)) { return updateUrlParams(url, redirectParams); } 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; const membership = params[PARAM_KEY_MEMBERSHIP]?.[0] || null; return { runnerType, membership: membership || DEFAULT_MEMBERSHIP, 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, membership = null, filters = [], sort = null, pagination = {} }, url = window.location.href, ) => { const filterParams = { // Defaults [PARAM_KEY_STATUS]: [], [PARAM_KEY_RUNNER_TYPE]: [], [PARAM_KEY_MEMBERSHIP]: [], [PARAM_KEY_TAG]: [], // Current filters ...filterToQueryObject(processFilters(filters), { filteredSearchTermKey: PARAM_KEY_SEARCH, }), }; if (runnerType) { filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType]; } if (membership && membership !== DEFAULT_MEMBERSHIP) { filterParams[PARAM_KEY_MEMBERSHIP] = [membership]; } if (!filterParams[PARAM_KEY_SEARCH]) { filterParams[PARAM_KEY_SEARCH] = null; } const isDefaultSort = sort !== DEFAULT_SORT; const otherParams = { // Sorting & Pagination [PARAM_KEY_SORT]: isDefaultSort ? sort : null, [PARAM_KEY_BEFORE]: pagination?.before || null, [PARAM_KEY_AFTER]: pagination?.after || null, }; 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, membership = 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 (membership) { filterVariables.membership = membership; } if (sort) { filterVariables.sort = sort; } const paginationVariables = getPaginationVariables(pagination, RUNNER_PAGE_SIZE); return { ...filterVariables, ...paginationVariables, }; }; /** * Decides whether or not a search object is the "default" or empty. * * A search is filtered if the user has entered filtering criteria. * * @param {Object} search * @returns true if this search is filtered, false otherwise */ export const isSearchFiltered = ({ runnerType = null, filters = [], pagination = {} } = {}) => { return Boolean( runnerType !== null || filters?.length !== 0 || pagination?.before || pagination?.after, ); };