Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/runner/runner_list')
-rw-r--r--app/assets/javascripts/runner/runner_list/index.js42
-rw-r--r--app/assets/javascripts/runner/runner_list/runner_list_app.vue127
-rw-r--r--app/assets/javascripts/runner/runner_list/runner_search_utils.js109
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;
+};