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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-06 15:10:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-06 15:10:15 +0300
commit14b5bf2629cf6cd77fe9bb6108d4a6dc0963f6c1 (patch)
tree90544438a276b7cd3c9b24ab92f46927ef768185 /app
parentff490fb7dfd0f4fedbd45b9079fcfa13ade3144c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/dev_ops_report_overview.svg (renamed from app/views/shared/icons/_dev_ops_report_overview.svg)0
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/devops_score.vue83
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue55
-rw-r--r--app/assets/javascripts/analytics/devops_report/constants.js11
-rw-r--r--app/assets/javascripts/analytics/devops_report/devops_score.js8
-rw-r--r--app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js36
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue84
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue37
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue42
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js3
-rw-r--r--app/assets/javascripts/pages/admin/runners/index/index.js16
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue3
-rw-r--r--app/controllers/admin/runners_controller.rb8
-rw-r--r--app/models/concerns/each_batch.rb6
-rw-r--r--app/models/integrations/datadog.rb14
-rw-r--r--app/views/admin/dev_ops_report/_callout.html.haml13
-rw-r--r--app/views/admin/dev_ops_report/_report.html.haml5
-rw-r--r--app/views/admin/runners/_runner.html.haml80
-rw-r--r--app/views/admin/runners/index.html.haml138
-rw-r--r--app/views/groups/runners/_settings.html.haml2
-rw-r--r--app/views/groups/runners/_sort_dropdown.html.haml (renamed from app/views/admin/runners/_sort_dropdown.html.haml)1
22 files changed, 260 insertions, 395 deletions
diff --git a/app/views/shared/icons/_dev_ops_report_overview.svg b/app/assets/images/dev_ops_report_overview.svg
index 2f31113bad7..2f31113bad7 100644
--- a/app/views/shared/icons/_dev_ops_report_overview.svg
+++ b/app/assets/images/dev_ops_report_overview.svg
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
index 1a3289ffb75..be38b32c119 100644
--- a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
@@ -2,6 +2,7 @@
import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { sprintf, s__ } from '~/locale';
+import DevopsScoreCallout from './devops_score_callout.vue';
const defaultHeaderAttrs = {
thClass: 'gl-bg-white!',
@@ -15,6 +16,7 @@ export default {
GlSingleStat,
GlLink,
GlEmptyState,
+ DevopsScoreCallout,
},
inject: {
devopsScoreMetrics: {
@@ -65,46 +67,49 @@ export default {
};
</script>
<template>
- <gl-empty-state
- v-if="isEmpty"
- :title="__('Data is still calculating...')"
- :svg-path="noDataImagePath"
- >
- <template #description>
- <p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
- <gl-link :href="devopsReportDocsPath">{{
- __('See example DevOps Score page in our documentation.')
- }}</gl-link>
- </template>
- </gl-empty-state>
- <div v-else data-testid="devops-score-app">
- <div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
- {{ titleHelperText }}
- </div>
- <gl-single-stat
- unit="%"
- size="sm"
- :title="s__('DevopsReport|Your score')"
- :should-animate="true"
- :value="devopsScoreMetrics.averageScore.value"
- :meta-icon="devopsScoreMetrics.averageScore.scoreLevel.icon"
- :meta-text="devopsScoreMetrics.averageScore.scoreLevel.label"
- :variant="devopsScoreMetrics.averageScore.scoreLevel.variant"
- />
- <gl-table
- :fields="$options.tableHeaderFields"
- :items="devopsScoreMetrics.cards"
- thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
- stacked="sm"
+ <div data-testid="devops-score-container">
+ <devops-score-callout />
+ <gl-empty-state
+ v-if="isEmpty"
+ :title="__('Data is still calculating...')"
+ :svg-path="noDataImagePath"
>
- <template #cell(usage)="{ item }">
- <div data-testid="usageCol">
- <span>{{ item.usage }}</span>
- <gl-badge :variant="item.scoreLevel.variant" size="sm" class="gl-ml-1">{{
- item.scoreLevel.label
- }}</gl-badge>
- </div>
+ <template #description>
+ <p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
+ <gl-link :href="devopsReportDocsPath">{{
+ __('See example DevOps Score page in our documentation.')
+ }}</gl-link>
</template>
- </gl-table>
+ </gl-empty-state>
+ <div v-else data-testid="devops-score-app">
+ <div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
+ {{ titleHelperText }}
+ </div>
+ <gl-single-stat
+ unit="%"
+ size="sm"
+ :title="s__('DevopsReport|Your score')"
+ :should-animate="true"
+ :value="devopsScoreMetrics.averageScore.value"
+ :meta-icon="devopsScoreMetrics.averageScore.scoreLevel.icon"
+ :meta-text="devopsScoreMetrics.averageScore.scoreLevel.label"
+ :variant="devopsScoreMetrics.averageScore.scoreLevel.variant"
+ />
+ <gl-table
+ :fields="$options.tableHeaderFields"
+ :items="devopsScoreMetrics.cards"
+ thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
+ stacked="sm"
+ >
+ <template #cell(usage)="{ item }">
+ <div data-testid="usageCol">
+ <span>{{ item.usage }}</span>
+ <gl-badge :variant="item.scoreLevel.variant" size="sm" class="gl-ml-1">{{
+ item.scoreLevel.label
+ }}</gl-badge>
+ </div>
+ </template>
+ </gl-table>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue b/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue
new file mode 100644
index 00000000000..e594b4e360a
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils';
+import {
+ INTRO_COOKIE_KEY,
+ INTRO_BANNER_TITLE,
+ INTRO_BANNER_BODY,
+ INTRO_BANNER_ACTION_TEXT,
+} from '../constants';
+
+export default {
+ name: 'DevopsScoreCallout',
+ components: {
+ GlBanner,
+ },
+ inject: {
+ devopsReportDocsPath: {
+ default: '',
+ },
+ devopsScoreIntroImagePath: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ bannerDismissed: parseBoolean(getCookie(INTRO_COOKIE_KEY)),
+ };
+ },
+ i18n: {
+ title: INTRO_BANNER_TITLE,
+ body: INTRO_BANNER_BODY,
+ action: INTRO_BANNER_ACTION_TEXT,
+ },
+ methods: {
+ dismissBanner() {
+ setCookie(INTRO_COOKIE_KEY, 'true');
+ this.bannerDismissed = true;
+ },
+ },
+};
+</script>
+<template>
+ <gl-banner
+ v-if="!bannerDismissed"
+ class="gl-mt-3"
+ variant="introduction"
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.action"
+ :button-link="devopsReportDocsPath"
+ :svg-path="devopsScoreIntroImagePath"
+ @close="dismissBanner"
+ >
+ <p>{{ $options.i18n.body }}</p>
+ </gl-banner>
+</template>
diff --git a/app/assets/javascripts/analytics/devops_report/constants.js b/app/assets/javascripts/analytics/devops_report/constants.js
new file mode 100644
index 00000000000..b395d7eb464
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/constants.js
@@ -0,0 +1,11 @@
+import { __ } from '~/locale';
+
+export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
+
+export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Report');
+
+export const INTRO_BANNER_BODY = __(
+ 'Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.',
+);
+
+export const INTRO_BANNER_ACTION_TEXT = __('Read more');
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_report/devops_score.js
index 18f7cf0c3ab..20e9fc776bc 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score.js
+++ b/app/assets/javascripts/analytics/devops_report/devops_score.js
@@ -6,7 +6,12 @@ export default () => {
if (!el) return false;
- const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
+ const {
+ devopsScoreMetrics,
+ devopsReportDocsPath,
+ noDataImagePath,
+ devopsScoreIntroImagePath,
+ } = el.dataset;
return new Vue({
el,
@@ -14,6 +19,7 @@ export default () => {
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
devopsReportDocsPath,
noDataImagePath,
+ devopsScoreIntroImagePath,
},
render(h) {
return h(DevopsScore);
diff --git a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
deleted file mode 100644
index 42d0fbacca0..00000000000
--- a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { __ } from '~/locale';
-import FilteredSearchTokenKeys from './filtered_search_token_keys';
-
-const tokenKeys = [
- {
- formattedKey: __('Status'),
- key: 'status',
- type: 'string',
- param: 'status',
- symbol: '',
- icon: 'messages',
- tag: 'status',
- },
- {
- formattedKey: __('Type'),
- key: 'type',
- type: 'string',
- param: 'type',
- symbol: '',
- icon: 'cube',
- tag: 'type',
- },
- {
- formattedKey: __('Tag'),
- key: 'tag',
- type: 'array',
- param: 'name[]',
- symbol: '~',
- icon: 'tag',
- tag: '~tag',
- },
-];
-
-const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
-
-export default AdminRunnersFilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
index ae5558111e6..707f186d4da 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -20,12 +20,12 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
// import DependencyRow from '~/packages/details/components/dependency_row.vue';
-// import InstallationCommands from '~/packages/details/components/installation_commands.vue';
// import PackageFiles from '~/packages/details/components/package_files.vue';
// import PackageListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { packageTypeToTrackCategory } from '~/packages/shared/utils';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
+import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import {
PACKAGE_TYPE_NUGET,
@@ -62,7 +62,7 @@ export default {
// DependencyRow,
PackageHistory,
AdditionalMetadata,
- // InstallationCommands,
+ InstallationCommands,
// PackageFiles,
},
directives: {
@@ -240,11 +240,7 @@ export default {
<div v-if="!isLoading" data-qa-selector="package_information_content">
<package-history :package-entity="packageEntity" :project-name="projectName" />
- <!-- <installation-commands
- :package-entity="packageEntity"
- :npm-path="npmPath"
- :npm-help-path="npmHelpPath"
- /> -->
+ <installation-commands :package-entity="packageEntity" />
<additional-metadata :package-entity="packageEntity" />
</div>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
index c178d3e97e9..47081e23318 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
@@ -1,9 +1,17 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
-import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import { NpmManager, TrackingActions, TrackingLabels } from '~/packages/details/constants';
+
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
+ TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ NPM_PACKAGE_MANAGER,
+ YARN_PACKAGE_MANAGER,
+} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
export default {
@@ -14,40 +22,70 @@ export default {
GlLink,
GlSprintf,
},
+ inject: ['npmHelpPath', 'npmPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
data() {
return {
- instructionType: 'npm',
+ instructionType: NPM_PACKAGE_MANAGER,
};
},
computed: {
- ...mapState(['npmHelpPath']),
- ...mapGetters(['npmInstallationCommand', 'npmSetupCommand']),
npmCommand() {
- return this.npmInstallationCommand(NpmManager.NPM);
+ return this.npmInstallationCommand(NPM_PACKAGE_MANAGER);
},
npmSetup() {
- return this.npmSetupCommand(NpmManager.NPM);
+ return this.npmSetupCommand(NPM_PACKAGE_MANAGER);
},
yarnCommand() {
- return this.npmInstallationCommand(NpmManager.YARN);
+ return this.npmInstallationCommand(YARN_PACKAGE_MANAGER);
},
yarnSetupCommand() {
- return this.npmSetupCommand(NpmManager.YARN);
+ return this.npmSetupCommand(YARN_PACKAGE_MANAGER);
},
showNpm() {
- return this.instructionType === 'npm';
+ return this.instructionType === NPM_PACKAGE_MANAGER;
+ },
+ },
+ methods: {
+ npmInstallationCommand(type) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ const instruction = type === NPM_PACKAGE_MANAGER ? 'npm i' : 'yarn add';
+
+ return `${instruction} ${this.packageEntity.name}`;
},
+ npmSetupCommand(type) {
+ const scope = this.packageEntity.name.substring(0, this.packageEntity.name.indexOf('/'));
+
+ if (type === NPM_PACKAGE_MANAGER) {
+ return `echo ${scope}:registry=${this.npmPath}/ >> .npmrc`;
+ }
+
+ return `echo \\"${scope}:registry\\" \\"${this.npmPath}/\\" >> .yarnrc`;
+ },
+ },
+ packageManagers: {
+ NPM_PACKAGE_MANAGER,
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
+ TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
},
i18n: {
helpText: s__(
'PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more.',
),
},
- trackingActions: { ...TrackingActions },
- TrackingLabels,
installOptions: [
- { value: 'npm', label: s__('PackageRegistry|Show NPM commands') },
- { value: 'yarn', label: s__('PackageRegistry|Show Yarn commands') },
+ { value: NPM_PACKAGE_MANAGER, label: s__('PackageRegistry|Show NPM commands') },
+ { value: YARN_PACKAGE_MANAGER, label: s__('PackageRegistry|Show Yarn commands') },
],
};
</script>
@@ -55,7 +93,7 @@ export default {
<template>
<div>
<installation-title
- package-type="npm"
+ :package-type="$options.packageManagers.NPM_PACKAGE_MANAGER"
:options="$options.installOptions"
@change="instructionType = $event"
/>
@@ -64,16 +102,16 @@ export default {
v-if="showNpm"
:instruction="npmCommand"
:copy-text="s__('PackageRegistry|Copy npm command')"
- :tracking-action="$options.trackingActions.COPY_NPM_INSTALL_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<code-instruction
v-else
:instruction="yarnCommand"
:copy-text="s__('PackageRegistry|Copy yarn command')"
- :tracking-action="$options.trackingActions.COPY_YARN_INSTALL_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
@@ -82,16 +120,16 @@ export default {
v-if="showNpm"
:instruction="npmSetup"
:copy-text="s__('PackageRegistry|Copy npm setup command')"
- :tracking-action="$options.trackingActions.COPY_NPM_SETUP_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NPM_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<code-instruction
v-else
:instruction="yarnSetupCommand"
:copy-text="s__('PackageRegistry|Copy yarn setup command')"
- :tracking-action="$options.trackingActions.COPY_YARN_SETUP_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_YARN_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
index 84493790b4d..2e9991b7be5 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
@@ -1,9 +1,12 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
-import { mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
-import { TrackingActions, TrackingLabels } from '~/packages/details/constants';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
export default {
@@ -14,17 +17,31 @@ export default {
GlLink,
GlSprintf,
},
+ inject: ['nugetHelpPath', 'nugetPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
computed: {
- ...mapState(['nugetHelpPath']),
- ...mapGetters(['nugetInstallationCommand', 'nugetSetupCommand']),
+ nugetInstallationCommand() {
+ return `nuget install ${this.packageEntity.name} -Source "GitLab"`;
+ },
+ nugetSetupCommand() {
+ return `nuget source Add -Name "GitLab" -Source "${this.nugetPath}" -UserName <your_username> -Password <your_token>`;
+ },
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
},
i18n: {
helpText: s__(
'PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}.',
),
},
- trackingActions: { ...TrackingActions },
- TrackingLabels,
installOptions: [{ value: 'nuget', label: s__('PackageRegistry|Show Nuget commands') }],
};
</script>
@@ -37,8 +54,8 @@ export default {
:label="s__('PackageRegistry|NuGet Command')"
:instruction="nugetInstallationCommand"
:copy-text="s__('PackageRegistry|Copy NuGet Command')"
- :tracking-action="$options.trackingActions.COPY_NUGET_INSTALL_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
@@ -46,8 +63,8 @@ export default {
:label="s__('PackageRegistry|Add NuGet Source')"
:instruction="nugetSetupCommand"
:copy-text="s__('PackageRegistry|Copy NuGet Setup Command')"
- :tracking-action="$options.trackingActions.COPY_NUGET_SETUP_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
index 21eba44a720..669adab9df6 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
@@ -1,9 +1,13 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
-import { mapGetters, mapState } from 'vuex';
+
import { s__ } from '~/locale';
-import { TrackingActions, TrackingLabels } from '~/packages/details/constants';
import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
export default {
@@ -14,9 +18,29 @@ export default {
GlLink,
GlSprintf,
},
+ inject: ['pypiHelpPath', 'pypiPath', 'pypiSetupPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
computed: {
- ...mapState(['pypiHelpPath']),
- ...mapGetters(['pypiPipCommand', 'pypiSetupCommand']),
+ pypiPipCommand() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `pip install ${this.packageEntity.name} --extra-index-url ${this.pypiPath}`;
+ },
+ pypiSetupCommand() {
+ return `[gitlab]
+repository = ${this.pypiSetupPath}
+username = __token__
+password = <your personal access token>`;
+ },
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
},
i18n: {
setupText: s__(
@@ -26,8 +50,6 @@ export default {
'PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}.',
),
},
- trackingActions: { ...TrackingActions },
- TrackingLabels,
installOptions: [{ value: 'pypi', label: s__('PackageRegistry|Show PyPi commands') }],
};
</script>
@@ -41,8 +63,8 @@ export default {
:instruction="pypiPipCommand"
:copy-text="s__('PackageRegistry|Copy Pip command')"
data-testid="pip-command"
- :tracking-action="$options.trackingActions.COPY_PIP_INSTALL_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
@@ -59,8 +81,8 @@ export default {
:copy-text="s__('PackageRegistry|Copy .pypirc content')"
data-testid="pypi-setup-content"
multiline
- :tracking-action="$options.trackingActions.COPY_PYPI_SETUP_COMMAND"
- :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index 0008e5f0c98..c1de9e7a61e 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -83,3 +83,6 @@ export const PACKAGE_ERROR_STATUS = 'error';
export const PACKAGE_DEFAULT_STATUS = 'default';
export const PACKAGE_HIDDEN_STATUS = 'hidden';
export const PACKAGE_PROCESSING_STATUS = 'processing';
+
+export const NPM_PACKAGE_MANAGER = 'npm';
+export const YARN_PACKAGE_MANAGER = 'yarn';
diff --git a/app/assets/javascripts/pages/admin/runners/index/index.js b/app/assets/javascripts/pages/admin/runners/index/index.js
index 8e7861c300a..f83111b6385 100644
--- a/app/assets/javascripts/pages/admin/runners/index/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index/index.js
@@ -1,17 +1,3 @@
-import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
-import { FILTERED_SEARCH } from '~/pages/constants';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
-import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import { initAdminRunners } from '~/runner/admin_runners';
-initFilteredSearch({
- page: FILTERED_SEARCH.ADMIN_RUNNERS,
- filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
- useDefaultState: true,
-});
-
-initInstallRunner();
-
-if (gon.features?.runnerListViewVueUi) {
- initAdminRunners();
-}
+initAdminRunners();
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 0b8408643ac..db84e2b5912 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -247,7 +247,8 @@ export default {
return items;
},
renderAddToTreeDropdown() {
- return this.canCollaborate || this.canCreateMrFromFork;
+ const isBlobPath = this.$route.name === 'blobPath' || this.$route.name === 'blobPathDecoded';
+ return !isBlobPath && (this.canCollaborate || this.canCreateMrFromFork);
},
},
methods: {
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index d1c91d9617f..8c74352a179 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -4,19 +4,11 @@ class Admin::RunnersController < Admin::ApplicationController
include RunnerSetupScripts
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
- before_action only: [:index] do
- push_frontend_feature_flag(:runner_list_view_vue_ui, current_user, default_enabled: :yaml)
- end
feature_category :runner
- NUMBER_OF_RUNNERS_PER_PAGE = 30
-
def index
- finder = Ci::RunnersFinder.new(current_user: current_user, params: params)
- @runners = finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
@active_runners_count = Ci::Runner.online.count
- @sort = finder.sort_key
end
def show
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index a59f00d73ec..443e1ab53b4 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -91,7 +91,11 @@ module EachBatch
# Any ORDER BYs are useless for this relation and can lead to less
# efficient UPDATE queries, hence we get rid of it.
- yield relation.except(:order), index
+ relation = relation.except(:order)
+
+ # Using unscoped is necessary to prevent leaking the current scope used by
+ # ActiveRecord to chain `each_batch` method.
+ unscoped { yield relation, index }
break unless stop
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index 8a356cf8abc..08cd451db94 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -129,14 +129,12 @@ module Integrations
end
def test(data)
- begin
- result = execute(data)
- return { success: false, result: result[:message] } if result[:http_status] != 200
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result[:message] }
+ result = execute(data)
+
+ {
+ success: (200..299).cover?(result[:http_status]),
+ result: result[:message]
+ }
end
private
diff --git a/app/views/admin/dev_ops_report/_callout.html.haml b/app/views/admin/dev_ops_report/_callout.html.haml
deleted file mode 100644
index 2b4c258a00c..00000000000
--- a/app/views/admin/dev_ops_report/_callout.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-.gl-mt-3
-.user-callout{ data: { uid: 'dev_ops_report_intro_callout_dismissed' } }
- .bordered-box.landing.content-block
- %button.gl-button.btn.btn-default-tertiary.close.js-close-callout{ type: 'button',
- 'aria-label' => _('Dismiss DevOps Report introduction') }
- = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
- .user-callout-copy
- %h4
- = _('Introducing Your DevOps Report')
- %p
- = _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.')
- .svg-container.devops
- = custom_icon('dev_ops_report_overview')
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_report.html.haml
index 0b26548d6e6..5ab68a463ba 100644
--- a/app/views/admin/dev_ops_report/_report.html.haml
+++ b/app/views/admin/dev_ops_report/_report.html.haml
@@ -1,9 +1,6 @@
- service_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
-- if service_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
- = render 'callout'
-
- if !service_ping_enabled
#js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/service_ping/index.md') } }
- else
- #js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, devops_report_docs_path: help_page_path('user/admin_area/analytics/dev_ops_report'), no_data_image_path: image_path('dev_ops_report_no_data.svg') } }
+ #js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, devops_report_docs_path: help_page_path('user/admin_area/analytics/dev_ops_report'), no_data_image_path: image_path('dev_ops_report_no_data.svg'), devops_score_intro_image_path: image_path('dev_ops_report_overview.svg') } }
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
deleted file mode 100644
index ce143a6b155..00000000000
--- a/app/views/admin/runners/_runner.html.haml
+++ /dev/null
@@ -1,80 +0,0 @@
--# Note: This file should stay aligned with:
--# `app/views/groups/runners/_runner.html.haml`
-
-.gl-responsive-table-row{ data: { testid: "runner-row-#{runner.id}" } }
- .table-section.section-10.section-wrap
- .table-mobile-header{ role: 'rowheader' }= _('Type')
- .table-mobile-content
- - if runner.instance_type?
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|shared')
- - elsif runner.group_type?
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|group')
- - else
- %span.badge.badge-pill.gl-badge.sm.badge-info= s_('Runners|specific')
- - if runner.locked?
- %span.badge.badge-pill.gl-badge.sm.badge-warning= s_('Runners|locked')
- - unless runner.active?
- %span.badge.badge-pill.gl-badge.sm.badge-danger= s_('Runners|paused')
-
- .table-section.section-30
- .table-mobile-header{ role: 'rowheader' }= s_('Runners|Runner')
- .table-mobile-content
- = link_to("##{runner.id} (#{runner.short_sha})", admin_runner_path(runner))
- .gl-text-truncate
- %span{ title: runner.description, data: { toggle: 'tooltip', container: 'body' } }
- = runner.description
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('Version')
- .table-mobile-content.str-truncated.has-tooltip{ title: runner.version }
- = runner.version
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('IP Address')
- .table-mobile-content.str-truncated.has-tooltip{ title: runner.ip_address }
- = runner.ip_address
-
- .table-section.section-5
- .table-mobile-header{ role: 'rowheader' }= _('Projects')
- .table-mobile-content
- - if runner.instance_type? || runner.group_type?
- = _('n/a')
- - else
- = runner.projects.count(:all)
-
- .table-section.section-5
- .table-mobile-header{ role: 'rowheader' }= _('Jobs')
- .table-mobile-content
- = limited_counter_with_delimiter(runner.builds)
-
- .table-section.section-10.section-wrap
- .table-mobile-header{ role: 'rowheader' }= _('Tags')
- .table-mobile-content
- - runner.tags.map(&:name).sort.each do |tag|
- %span.badge.badge-primary.str-truncated.has-tooltip{ title: tag }
- = tag
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('Last contact')
- .table-mobile-content
- - contacted_at = runner_contacted_at(runner)
- - if contacted_at
- = time_ago_with_tooltip contacted_at
- - else
- = _('Never')
-
- .table-section.table-button-footer.section-10
- .btn-group.table-action-buttons
- .btn-group
- = link_to admin_runner_path(runner), class: 'gl-button btn btn-default btn-icon has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
- = sprite_icon('pencil', css_class: 'gl-icon')
- .btn-group
- - if runner.active?
- = link_to [:pause, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-icon has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = sprite_icon('pause', css_class: 'gl-icon')
- - else
- = link_to [:resume, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-icon has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
- = sprite_icon('play', css_class: 'gl-icon')
- .btn-group
- = link_to [:admin, runner], method: :delete, class: 'gl-button btn btn-danger btn-icon has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = sprite_icon('close', css_class: 'gl-icon')
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 5dce44a787c..f298fce7bcf 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,140 +1,4 @@
- breadcrumb_title _('Runners')
- page_title _('Runners')
-- if Feature.enabled?(:runner_list_view_vue_ui, current_user, default_enabled: :yaml)
- #js-admin-runners{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
-- else
- .row
- .col-sm-6
- .bs-callout
- %p
- = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
- %br
- = _('You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want.')
- %br
-
- %div
- %span= _('Runners can be:')
- %ul
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|shared')
- \-
- = _('Runs jobs from all unassigned projects.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|group')
- \-
- = _('Runs jobs from all unassigned projects in its group.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-info= s_('Runners|specific')
- \-
- = _('Runs jobs from assigned projects.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-warning= s_('Runners|locked')
- \-
- = _('Cannot be assigned to other projects.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-danger= s_('Runners|paused')
- \-
- = _('Not available to run jobs.')
-
- .col-sm-6
- .bs-callout
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
- type: s_('Runners|shared'),
- reset_token_url: reset_registration_token_admin_application_settings_path,
- project_path: '',
- group_path: '' }
-
- .row
- .col-sm-9
- = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
- .filtered-search-wrapper.d-flex
- .filtered-search-box
- = dropdown_tag(_('Recent searches'),
- options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
- toggle_class: 'gl-button btn btn-default filtered-search-history-dropdown-toggle-button',
- dropdown_class: 'filtered-search-history-dropdown',
- content_class: 'filtered-search-history-dropdown-content' }) do
- .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
- .filtered-search-box-input-container.droplab-dropdown
- .scroll-container
- %ul.tokens-container.list-unstyled
- %li.input-token
- %input.form-control.filtered-search{ search_filter_input_options('runners') }
- #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
- = button_tag class: %w[gl-button btn btn-link] do
- -# Encapsulate static class name `{{icon}}` inside #{} to bypass
- -# haml lint's ClassAttributeWithStaticValue
- %svg
- %use{ 'xlink:href': "#{'{{icon}}'}" }
- %span.js-filter-hint
- {{formattedKey}}
- #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu
- %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
- %li.filter-dropdown-item{ data: { value: "{{ title }}" } }
- %button.gl-button.btn.btn-link{ type: 'button' }
- {{ title }}
- %span.btn-helptext
- {{ help }}
- #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_STATUSES.each do |status|
- %li.filter-dropdown-item{ data: { value: status } }
- = button_tag class: %w[gl-button btn btn-link] do
- = status.titleize
-
- #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
- %li.filter-dropdown-item{ data: { value: runner_type } }
- = button_tag class: %w[gl-button btn btn-link] do
- = runner_type.titleize
-
- #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
- %li.filter-dropdown-item{ data: { value: runner_type } }
- = button_tag class: %w[gl-button btn btn-link] do
- = runner_type.titleize
-
- #js-dropdown-runner-tag.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
- %button.gl-button.btn.btn-link
- = _('No Tag')
- %li.divider.droplab-item-ignore
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.gl-button.btn.btn-link.js-data-value
- %span.dropdown-light-content
- {{name}}
-
- = button_tag class: %w[clear-search hidden] do
- = sprite_icon('close', size: 16, css_class: 'clear-search-icon')
- .filter-dropdown-container
- = render 'sort_dropdown'
-
- .col-sm-3.text-right-lg
- = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
- - if @runners.any?
- .content-list{ data: { testid: 'runners-table' } }
- .table-holder
- .gl-responsive-table-row.table-row-header{ role: 'row' }
- .table-section.section-10{ role: 'rowheader' }= _('Type/State')
- .table-section.section-30{ role: 'rowheader' }= s_('Runners|Runner')
- .table-section.section-10{ role: 'rowheader' }= _('Version')
- .table-section.section-10{ role: 'rowheader' }= _('IP Address')
- .table-section.section-5{ role: 'rowheader' }= _('Projects')
- .table-section.section-5{ role: 'rowheader' }= _('Jobs')
- .table-section.section-10{ role: 'rowheader' }= _('Tags')
- .table-section.section-10{ role: 'rowheader' }= _('Last contact')
- .table-section.section-10{ role: 'rowheader' }
-
- - @runners.each do |runner|
- = render 'admin/runners/runner', runner: runner
- = paginate @runners, theme: 'gitlab'
- - else
- .nothing-here-block= _('No runners found')
+#js-admin-runners{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
diff --git a/app/views/groups/runners/_settings.html.haml b/app/views/groups/runners/_settings.html.haml
index 187588f5f11..50982fa381c 100644
--- a/app/views/groups/runners/_settings.html.haml
+++ b/app/views/groups/runners/_settings.html.haml
@@ -75,7 +75,7 @@
= button_tag class: 'clear-search hidden' do
= sprite_icon('close', size: 16, css_class: 'clear-search-icon')
.filter-dropdown-container
- = render 'admin/runners/sort_dropdown'
+ = render 'groups/runners/sort_dropdown'
.col-sm-3.text-right-lg
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: limited_counter_with_delimiter(@all_group_runners.online) }
diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/groups/runners/_sort_dropdown.html.haml
index c6627ae0f27..eb826552211 100644
--- a/app/views/admin/runners/_sort_dropdown.html.haml
+++ b/app/views/groups/runners/_sort_dropdown.html.haml
@@ -8,4 +8,3 @@
%li
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sorted_by)
= sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date), sorted_by)
-