Commit de861e89 authored by Miguel Rincon's avatar Miguel Rincon

Load runner webUrl from GraphQL

This change removes technical debt by loading the "webUrl"
of a runner from the API instead of hardcoding it.

The Runner link will vary for the admin and the group users
respectively.
parent 93aeeb73
<script>
import { GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
......@@ -6,6 +7,7 @@ import { formatNumber, sprintf, __ } from '~/locale';
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 RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
......@@ -23,10 +25,12 @@ import { captureException } from '../sentry_utils';
export default {
name: 'AdminRunnersApp',
components: {
GlLink,
RunnerFilteredSearchBar,
RunnerList,
RunnerManualSetupHelp,
RunnerTypeHelp,
RunnerName,
RunnerPagination,
},
props: {
......@@ -150,7 +154,13 @@ export default {
{{ __('No runners found') }}
</div>
<template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading" />
<runner-list :runners="runners.items" :loading="runnersLoading">
<template #runner-name="{ runner }">
<gl-link :href="runner.adminUrl">
<runner-name :runner="runner" />
</gl-link>
</template>
</runner-list>
<runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
</template>
</div>
......
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
......@@ -37,13 +36,6 @@ export default {
};
},
computed: {
runnerNumericalId() {
return getIdFromGraphQLId(this.runner.id);
},
runnerUrl() {
// TODO implement using webUrl from the API
return `${gon.gitlab_url || ''}/admin/runners/${this.runnerNumericalId}`;
},
isActive() {
return this.runner.active;
},
......@@ -119,7 +111,7 @@ export default {
},
},
awaitRefetchQueries: true,
refetchQueries: ['getRunners'],
refetchQueries: ['getRunners', 'getGroupRunners'],
});
if (errors && errors.length) {
throw new Error(errors.join(' '));
......@@ -147,12 +139,20 @@ export default {
<template>
<gl-button-group>
<!--
This button appears for administratos: those with
access to the adminUrl. More advanced permissions policies
will allow more granular permissions.
See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
-->
<gl-button
v-if="runner.adminUrl"
v-gl-tooltip.hover.viewport
:href="runner.adminUrl"
:title="$options.i18n.I18N_EDIT"
:aria-label="$options.i18n.I18N_EDIT"
icon="pencil"
:href="runnerUrl"
data-testid="edit-runner"
/>
<gl-button
......
<script>
import { GlLink } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import RunnerName from '../runner_name.vue';
export default {
components: {
GlLink,
TooltipOnTruncate,
RunnerName,
},
props: {
runner: {
......@@ -15,26 +14,18 @@ export default {
},
},
computed: {
runnerNumericalId() {
return getIdFromGraphQLId(this.runner.id);
},
runnerUrl() {
// TODO implement using webUrl from the API
return `${gon.gitlab_url || ''}/admin/runners/${this.runnerNumericalId}`;
},
description() {
return this.runner.description;
},
shortSha() {
return this.runner.shortSha;
},
},
};
</script>
<template>
<div>
<gl-link :href="runnerUrl"> #{{ runnerNumericalId }} ({{ shortSha }})</gl-link>
<slot :runner="runner" name="runner-name">
<runner-name :runner="runner" />
</slot>
<tooltip-on-truncate class="gl-display-block" :title="description" truncate-target="child">
<div class="gl-text-truncate">
{{ description }}
......
......@@ -5,7 +5,7 @@ import { formatNumber, __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerNameCell from './cells/runner_name_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerTypeCell from './cells/runner_type_cell.vue';
import RunnerTags from './runner_tags.vue';
......@@ -35,7 +35,7 @@ export default {
GlSkeletonLoader,
TimeAgo,
RunnerActionsCell,
RunnerNameCell,
RunnerSummaryCell,
RunnerTags,
RunnerTypeCell,
},
......@@ -77,7 +77,7 @@ export default {
},
fields: [
tableField({ key: 'type', label: __('Type/State') }),
tableField({ key: 'name', label: s__('Runners|Runner'), width: 30 }),
tableField({ key: 'summary', label: s__('Runners|Runner'), width: 30 }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'ipAddress', label: __('IP Address') }),
tableField({ key: 'projectCount', label: __('Projects'), width: 5 }),
......@@ -107,8 +107,12 @@ export default {
<runner-type-cell :runner="item" />
</template>
<template #cell(name)="{ item }">
<runner-name-cell :runner="item" />
<template #cell(summary)="{ item, index }">
<runner-summary-cell :runner="item">
<template #runner-name="{ runner }">
<slot name="runner-name" :runner="runner" :index="index"></slot>
</template>
</runner-summary-cell>
</template>
<template #cell(version)="{ item: { version } }">
......
<script>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export default {
props: {
runner: {
type: Object,
required: true,
},
},
methods: {
getIdFromGraphQLId,
},
};
</script>
<template>
<span>#{{ getIdFromGraphQLId(runner.id) }} ({{ runner.shortSha }})</span>
</template>
......@@ -24,8 +24,11 @@ query getGroupRunners(
search: $search
sort: $sort
) {
nodes {
...RunnerNode
edges {
webUrl
node {
...RunnerNode
}
}
pageInfo {
...PageInfo
......
......@@ -25,6 +25,7 @@ query getRunners(
) {
nodes {
...RunnerNode
adminUrl
}
pageInfo {
...PageInfo
......
<script>
import { GlLink } from '@gitlab/ui';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, s__ } from '~/locale';
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 RunnerName from '../components/runner_name.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import {
......@@ -27,9 +31,11 @@ import { captureException } from '../sentry_utils';
export default {
name: 'GroupRunnersApp',
components: {
GlLink,
RunnerFilteredSearchBar,
RunnerList,
RunnerManualSetupHelp,
RunnerName,
RunnerTypeHelp,
RunnerPagination,
},
......@@ -51,6 +57,7 @@ export default {
return {
search: fromUrlQueryToSearch(),
runners: {
webUrls: [],
items: [],
pageInfo: {},
},
......@@ -68,8 +75,10 @@ export default {
},
update(data) {
const { runners } = data?.group || {};
return {
items: runners?.nodes || [],
webUrls: runners?.edges.map(({ webUrl }) => webUrl) || [],
items: runners?.edges.map(({ node }) => node) || [],
pageInfo: runners?.pageInfo || {},
};
},
......@@ -163,7 +172,13 @@ export default {
{{ __('No runners found') }}
</div>
<template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading" />
<runner-list :runners="runners.items" :loading="runnersLoading">
<template #runner-name="{ runner, index }">
<gl-link :href="runners.webUrls[index]">
<runner-name :runner="runner" />
</gl-link>
</template>
</runner-list>
<runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
</template>
</div>
......
import { GlLink } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -5,6 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
......@@ -98,6 +100,20 @@ describe('AdminRunnersApp', () => {
expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
});
it('runner item links to the runner admin page', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
const { id, shortSha } = runnersData.data.runners.nodes[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
expect(runnerLink.text()).toBe(`#${numericId} (${shortSha})`);
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${numericId}`);
});
it('requests the runners with no filters', () => {
expect(mockRunnersQuery).toHaveBeenLastCalledWith({
status: undefined,
......
......@@ -5,15 +5,18 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
import { runnerData } from '../../mock_data';
import { runnersData, runnerData } from '../../mock_data';
const mockRunner = runnerData.data.runner;
const mockRunner = runnersData.data.runners.nodes[0];
const mockRunnerDetails = runnerData.data.runner;
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value;
const localVue = createLocalVue();
localVue.use(VueApollo);
......@@ -36,6 +39,7 @@ describe('RunnerTypeCell', () => {
propsData: {
runner: {
id: mockRunner.id,
adminUrl: mockRunner.adminUrl,
active,
},
},
......@@ -61,7 +65,7 @@ describe('RunnerTypeCell', () => {
runnerUpdateMutationHandler.mockResolvedValue({
data: {
runnerUpdate: {
runner: runnerData.data.runner,
runner: mockRunnerDetails,
errors: [],
},
},
......@@ -78,7 +82,7 @@ describe('RunnerTypeCell', () => {
it('Displays the runner edit link with the correct href', () => {
createComponent();
expect(findEditBtn().attributes('href')).toBe('/admin/runners/1');
expect(findEditBtn().attributes('href')).toBe(mockRunner.adminUrl);
});
describe.each`
......@@ -231,7 +235,7 @@ describe('RunnerTypeCell', () => {
},
},
awaitRefetchQueries: true,
refetchQueries: [getRunnersQueryName],
refetchQueries: [getRunnersQueryName, getGroupRunnersQueryName],
});
});
......
import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import RunnerNameCell from '~/runner/components/cells/runner_name_cell.vue';
import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
const mockId = '1';
const mockShortSha = '2P6oDVDm';
......@@ -9,10 +8,8 @@ const mockDescription = 'runner-1';
describe('RunnerTypeCell', () => {
let wrapper;
const findLink = () => wrapper.findComponent(GlLink);
const createComponent = () => {
wrapper = mount(RunnerNameCell, {
const createComponent = (options) => {
wrapper = mount(RunnerSummaryCell, {
propsData: {
runner: {
id: `gid://gitlab/Ci::Runner/${mockId}`,
......@@ -20,6 +17,7 @@ describe('RunnerTypeCell', () => {
description: mockDescription,
},
},
...options,
});
};
......@@ -31,12 +29,23 @@ describe('RunnerTypeCell', () => {
wrapper.destroy();
});
it('Displays the runner link with id and short token', () => {
expect(findLink().text()).toBe(`#${mockId} (${mockShortSha})`);
expect(findLink().attributes('href')).toBe(`/admin/runners/${mockId}`);
it('Displays the runner name as id and short token', () => {
expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`);
});
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockDescription);
});
it('Displays a custom slot', () => {
const slotContent = 'My custom runner summary';
createComponent({
slots: {
'runner-name': slotContent,
},
});
expect(wrapper.text()).toContain(slotContent);
});
});
import { GlLink, GlTable, GlSkeletonLoader } from '@gitlab/ui';
import { GlTable, GlSkeletonLoader } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -67,11 +67,11 @@ describe('RunnerList', () => {
// Badges
expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused');
// Runner identifier
expect(findCell({ fieldKey: 'name' }).text()).toContain(
// Runner summary
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
`#${getIdFromGraphQLId(id)} (${shortSha})`,
);
expect(findCell({ fieldKey: 'name' }).text()).toContain(description);
expect(findCell({ fieldKey: 'summary' }).text()).toContain(description);
// Other fields
expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
......@@ -136,12 +136,11 @@ describe('RunnerList', () => {
});
});
it('Links to the runner page', () => {
const { id } = mockRunners[0];
it('Shows runner identifier', () => {
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
expect(findCell({ fieldKey: 'name' }).find(GlLink).attributes('href')).toBe(
`/admin/runners/${getIdFromGraphQLId(id)}`,
);
expect(findCell({ fieldKey: 'summary' }).text()).toContain(`#${numericId} (${shortSha})`);
});
describe('When data is loading', () => {
......
import { GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -5,6 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
......@@ -34,8 +36,7 @@ localVue.use(VueApollo);
const mockGroupFullPath = 'group1';
const mockRegistrationToken = 'AABBCC';
const mockRunners = groupRunnersData.data.group.runners.nodes;
const mockGroupRunnersLimitedCount = mockRunners.length;
const mockGroupRunnersLimitedCount = groupRunnersData.data.group.runners.edges.length;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
......@@ -91,7 +92,22 @@ describe('GroupRunnersApp', () => {
});
it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(groupRunnersData.data.group.runners.nodes);
expect(findRunnerList().props('runners')).toEqual(
groupRunnersData.data.group.runners.edges.map(({ node }) => node),
);
});
it('runner item links to the runner group page', async () => {
const { webUrl, node } = groupRunnersData.data.group.runners.edges[0];
const { id, shortSha } = node;
createComponent({ mountFn: mount });
await waitForPromises();
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
expect(runnerLink.text()).toBe(`#${getIdFromGraphQLId(id)} (${shortSha})`);
expect(runnerLink.attributes('href')).toBe(webUrl);
});
it('requests the runners with group path and no other filters', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment