diff options
Diffstat (limited to 'app/assets/javascripts/runner/runner_list')
3 files changed, 278 insertions, 0 deletions
diff --git a/app/assets/javascripts/runner/runner_list/index.js b/app/assets/javascripts/runner/runner_list/index.js new file mode 100644 index 00000000000..5eba14a7948 --- /dev/null +++ b/app/assets/javascripts/runner/runner_list/index.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import RunnerDetailsApp from './runner_list_app.vue'; + +Vue.use(VueApollo); + +export const initRunnerList = (selector = '#js-runner-list') => { + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + // TODO `activeRunnersCount` should be implemented using a GraphQL API. + const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient( + {}, + { + assumeImmutableResults: true, + }, + ), + }); + + return new Vue({ + el, + apolloProvider, + provide: { + runnerInstallHelpPage, + }, + render(h) { + return h(RunnerDetailsApp, { + props: { + activeRunnersCount: parseInt(activeRunnersCount, 10), + registrationToken, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/runner_list/runner_list_app.vue new file mode 100644 index 00000000000..b4eacb911a2 --- /dev/null +++ b/app/assets/javascripts/runner/runner_list/runner_list_app.vue @@ -0,0 +1,127 @@ +<script> +import * as Sentry from '@sentry/browser'; +import { fetchPolicies } from '~/lib/graphql'; +import { updateHistory } from '~/lib/utils/url_utility'; +import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; +import RunnerList from '../components/runner_list.vue'; +import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; +import RunnerPagination from '../components/runner_pagination.vue'; +import RunnerTypeHelp from '../components/runner_type_help.vue'; +import getRunnersQuery from '../graphql/get_runners.query.graphql'; +import { + fromUrlQueryToSearch, + fromSearchToUrl, + fromSearchToVariables, +} from './runner_search_utils'; + +export default { + components: { + RunnerFilteredSearchBar, + RunnerList, + RunnerManualSetupHelp, + RunnerTypeHelp, + RunnerPagination, + }, + props: { + activeRunnersCount: { + type: Number, + required: true, + }, + registrationToken: { + type: String, + required: true, + }, + }, + data() { + return { + search: fromUrlQueryToSearch(), + runners: { + items: [], + pageInfo: {}, + }, + }; + }, + apollo: { + runners: { + query: getRunnersQuery, + // Runners can be updated by users directly in this list. + // A "cache and network" policy prevents outdated filtered + // results. + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + variables() { + return this.variables; + }, + update(data) { + const { runners } = data; + return { + items: runners?.nodes || [], + pageInfo: runners?.pageInfo || {}, + }; + }, + error(err) { + this.captureException(err); + }, + }, + }, + computed: { + variables() { + return fromSearchToVariables(this.search); + }, + runnersLoading() { + return this.$apollo.queries.runners.loading; + }, + noRunnersFound() { + return !this.runnersLoading && !this.runners.items.length; + }, + }, + watch: { + search: { + deep: true, + handler() { + // TODO Implement back button reponse using onpopstate + updateHistory({ + url: fromSearchToUrl(this.search), + title: document.title, + }); + }, + }, + }, + errorCaptured(err) { + this.captureException(err); + }, + methods: { + captureException(err) { + Sentry.withScope((scope) => { + scope.setTag('component', 'runner_list_app'); + Sentry.captureException(err); + }); + }, + }, +}; +</script> +<template> + <div> + <div class="row"> + <div class="col-sm-6"> + <runner-type-help /> + </div> + <div class="col-sm-6"> + <runner-manual-setup-help :registration-token="registrationToken" /> + </div> + </div> + + <runner-filtered-search-bar v-model="search" namespace="admin_runners" /> + + <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> + {{ __('No runners found') }} + </div> + <template v-else> + <runner-list + :runners="runners.items" + :loading="runnersLoading" + :active-runners-count="activeRunnersCount" + /> + <runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/runner/runner_list/runner_search_utils.js b/app/assets/javascripts/runner/runner_list/runner_search_utils.js new file mode 100644 index 00000000000..e45972b81db --- /dev/null +++ b/app/assets/javascripts/runner/runner_list/runner_search_utils.js @@ -0,0 +1,109 @@ +import { queryToObject, setUrlParams } from '~/lib/utils/url_utility'; +import { + filterToQueryObject, + processFilters, + urlQueryToFilter, + prepareTokens, +} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; +import { + PARAM_KEY_SEARCH, + PARAM_KEY_STATUS, + PARAM_KEY_RUNNER_TYPE, + PARAM_KEY_SORT, + PARAM_KEY_PAGE, + PARAM_KEY_AFTER, + PARAM_KEY_BEFORE, + DEFAULT_SORT, + RUNNER_PAGE_SIZE, +} from '../constants'; + +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, + }; +}; + +export const fromUrlQueryToSearch = (query = window.location.search) => { + const params = queryToObject(query, { gatherArrays: true }); + + return { + filters: prepareTokens( + urlQueryToFilter(query, { + filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE], + filteredSearchTermKey: PARAM_KEY_SEARCH, + legacySpacesDecode: false, + }), + ), + sort: params[PARAM_KEY_SORT] || DEFAULT_SORT, + pagination: getPaginationFromParams(params), + }; +}; + +export const fromSearchToUrl = ( + { filters = [], sort = null, pagination = {} }, + url = window.location.href, +) => { + const filterParams = { + // Defaults + [PARAM_KEY_SEARCH]: null, + [PARAM_KEY_STATUS]: [], + [PARAM_KEY_RUNNER_TYPE]: [], + // Current filters + ...filterToQueryObject(processFilters(filters), { + filteredSearchTermKey: PARAM_KEY_SEARCH, + }), + }; + + 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); +}; + +export const fromSearchToVariables = ({ filters = [], sort = null, pagination = {} } = {}) => { + const variables = {}; + + const queryObj = filterToQueryObject(processFilters(filters), { + filteredSearchTermKey: PARAM_KEY_SEARCH, + }); + + variables.search = queryObj[PARAM_KEY_SEARCH]; + + // TODO Get more than one value when GraphQL API supports OR for "status" + [variables.status] = queryObj[PARAM_KEY_STATUS] || []; + + // TODO Get more than one value when GraphQL API supports OR for "runner type" + [variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || []; + + if (sort) { + variables.sort = sort; + } + + if (pagination.before) { + variables.before = pagination.before; + variables.last = RUNNER_PAGE_SIZE; + } else { + variables.after = pagination.after; + variables.first = RUNNER_PAGE_SIZE; + } + + return variables; +}; |