summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/ci/runner/runner_search_utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ci/runner/runner_search_utils.js')
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js267
1 files changed, 267 insertions, 0 deletions
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
new file mode 100644
index 00000000000..adc832b0600
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -0,0 +1,267 @@
+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,
+ );
+};