...@@ -26,7 +26,6 @@ export function getBoardSortableDefaultOptions(obj) { ...@@ -26,7 +26,6 @@ export function getBoardSortableDefaultOptions(obj) {
scrollSpeed: 20, scrollSpeed: 20,
onStart: sortableStart, onStart: sortableStart,
onEnd: sortableEnd, onEnd: sortableEnd,
fallbackTolerance: 1,
}); });
Object.keys(obj).forEach(key => { Object.keys(obj).forEach(key => {
... ...
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
GlFormSelect, GlFormSelect,
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormTextarea,
GlFormCheckbox, GlFormCheckbox,
GlLink, GlLink,
GlIcon, GlIcon,
...@@ -19,6 +20,7 @@ export default { ...@@ -19,6 +20,7 @@ export default {
GlFormSelect, GlFormSelect,
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormTextarea,
GlFormCheckbox, GlFormCheckbox,
GlLink, GlLink,
GlIcon, GlIcon,
...@@ -34,17 +36,29 @@ export default { ...@@ -34,17 +36,29 @@ export default {
'maskableRegex', 'maskableRegex',
]), ]),
canSubmit() { canSubmit() {
if (this.variableData.masked && this.maskedState === false) {
return false;
}
return this.variableData.key !== '' && this.variableData.secret_value !== ''; return this.variableData.key !== '' && this.variableData.secret_value !== '';
}, },
canMask() { canMask() {
const regex = RegExp(this.maskableRegex); const regex = RegExp(this.maskableRegex);
return regex.test(this.variableData.secret_value); return regex.test(this.variableData.secret_value);
}, },
displayMaskedError() {
return !this.canMask && this.variableData.masked && this.variableData.secret_value !== '';
},
maskedState() {
if (this.displayMaskedError) {
return false;
}
return null;
},
variableData() { variableData() {
return this.variableBeingEdited || this.variable; return this.variableBeingEdited || this.variable;
}, },
modalActionText() { modalActionText() {
return this.variableBeingEdited ? __('Update Variable') : __('Add variable'); return this.variableBeingEdited ? __('Update variable') : __('Add variable');
}, },
primaryAction() { primaryAction() {
return { return {
...@@ -52,11 +66,23 @@ export default { ...@@ -52,11 +66,23 @@ export default {
attributes: { variant: 'success', disabled: !this.canSubmit }, attributes: { variant: 'success', disabled: !this.canSubmit },
}; };
}, },
deleteAction() {
if (this.variableBeingEdited) {
return {
text: __('Delete variable'),
attributes: { variant: 'danger', category: 'secondary' },
};
}
return null;
},
cancelAction() { cancelAction() {
return { return {
text: __('Cancel'), text: __('Cancel'),
}; };
}, },
maskedFeedback() {
return __('This variable can not be masked');
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -65,6 +91,7 @@ export default { ...@@ -65,6 +91,7 @@ export default {
'resetEditing', 'resetEditing',
'displayInputValue', 'displayInputValue',
'clearModal', 'clearModal',
'deleteVariable',
]), ]),
updateOrAddVariable() { updateOrAddVariable() {
if (this.variableBeingEdited) { if (this.variableBeingEdited) {
...@@ -89,74 +116,93 @@ export default { ...@@ -89,74 +116,93 @@ export default {
:modal-id="$options.modalId" :modal-id="$options.modalId"
:title="modalActionText" :title="modalActionText"
:action-primary="primaryAction" :action-primary="primaryAction"
:action-secondary="deleteAction"
:action-cancel="cancelAction" :action-cancel="cancelAction"
@ok="updateOrAddVariable" @ok="updateOrAddVariable"
@hidden="resetModalHandler" @hidden="resetModalHandler"
@secondary="deleteVariable(variableBeingEdited)"
> >
<form> <form>
<gl-form-group label="Type" label-for="ci-variable-type"> <gl-form-group :label="__('Key')" label-for="ci-variable-key">
<gl-form-select
id="ci-variable-type"
v-model="variableData.variable_type"
:options="typeOptions"
/>
</gl-form-group>
<div class="d-flex">
<gl-form-group label="Key" label-for="ci-variable-key" class="w-50 append-right-15">
<gl-form-input <gl-form-input
id="ci-variable-key" id="ci-variable-key"
v-model="variableData.key" v-model="variableData.key"
type="text"
data-qa-selector="variable_key" data-qa-selector="variable_key"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group label="Value" label-for="ci-variable-value" class="w-50"> <gl-form-group
<gl-form-input :label="__('Value')"
label-for="ci-variable-value"
:state="maskedState"
:invalid-feedback="maskedFeedback"
>
<gl-form-textarea
id="ci-variable-value" id="ci-variable-value"
v-model="variableData.secret_value" v-model="variableData.secret_value"
type="text" rows="3"
max-rows="6"
data-qa-selector="variable_value" data-qa-selector="variable_value"
/> />
</gl-form-group> </gl-form-group>
</div>
<gl-form-group v-if="!isGroup" label="Environment scope" label-for="ci-variable-env"> <div class="d-flex">
<gl-form-group
:label="__('Type')"
label-for="ci-variable-type"
class="w-50 append-right-15"
:class="{ 'w-100': isGroup }"
>
<gl-form-select
id="ci-variable-type"
v-model="variableData.variable_type"
:options="typeOptions"
/>
</gl-form-group>
<gl-form-group
v-if="!isGroup"
:label="__('Environment scope')"
label-for="ci-variable-env"
class="w-50"
>
<gl-form-select <gl-form-select
id="ci-variable-env" id="ci-variable-env"
v-model="variableData.environment_scope" v-model="variableData.environment_scope"
:options="environments" :options="environments"
/> />
</gl-form-group> </gl-form-group>
</div>
<gl-form-group label="Flags" label-for="ci-variable-flags"> <gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
<gl-form-checkbox v-model="variableData.protected" class="mb-0"> <gl-form-checkbox v-model="variableData.protected" class="mb-0">
{{ __('Protect variable') }} {{ __('Protect variable') }}
<gl-link href="/help/ci/variables/README#protected-environment-variables"> <gl-link href="/help/ci/variables/README#protected-environment-variables">
<gl-icon name="question" :size="12" /> <gl-icon name="question" :size="12" />
</gl-link> </gl-link>
<p class="prepend-top-4 clgray"> <p class="prepend-top-4 text-secondary">
{{ __('Allow variables to run on protected branches and tags.') }} {{ __('Export variable to pipelines running on protected branches and tags only.') }}
</p> </p>
</gl-form-checkbox> </gl-form-checkbox>
<gl-form-checkbox <gl-form-checkbox
ref="masked-ci-variable" ref="masked-ci-variable"
v-model="variableData.masked" v-model="variableData.masked"
:disabled="!canMask"
data-qa-selector="variable_masked" data-qa-selector="variable_masked"
> >
{{ __('Mask variable') }} {{ __('Mask variable') }}
<gl-link href="/help/ci/variables/README#masked-variables"> <gl-link href="/help/ci/variables/README#masked-variables">
<gl-icon name="question" :size="12" /> <gl-icon name="question" :size="12" />
</gl-link> </gl-link>
<p class="prepend-top-4 append-bottom-0 clgray"> <p class="prepend-top-4 append-bottom-0 text-secondary">
{{ {{ __('Variable will be masked in job logs.') }}
__( <span
'Variables will be masked in job logs. Requires values to meet regular expression requirements.', :class="{
) 'bold text-plain': displayMaskedError,
}} }"
>
{{ __('Requires values to meet regular expression requirements.') }}</span
>
<gl-link href="/help/ci/variables/README#masked-variables">{{ <gl-link href="/help/ci/variables/README#masked-variables">{{
__('More information') __('More information')
}}</gl-link> }}</gl-link>
... ...
......
<script>
import { GlPopover, GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
export default {
maxTextLength: 95,
components: {
GlPopover,
GlIcon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
target: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
tooltipText: {
type: String,
required: true,
},
},
computed: {
displayValue() {
if (this.value.length > this.$options.maxTextLength) {
return `${this.value.substring(0, this.$options.maxTextLength)}...`;
}
return this.value;
},
},
};
</script>
<template>
<div id="popover-container">
<gl-popover :target="target" triggers="hover" placement="top" container="popover-container">
<div class="d-flex justify-content-between position-relative">
<div class="pr-5 w-100 ci-popover-value">{{ displayValue }}</div>
<gl-button
v-gl-tooltip
class="btn-transparent btn-clipboard position-absolute position-top-0 position-right-0"
:title="tooltipText"
:data-clipboard-text="value"
>
<gl-icon name="copy-to-clipboard" />
</gl-button>
</div>
</gl-popover>
</div>
</template>
...@@ -3,44 +3,58 @@ import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui'; ...@@ -3,44 +3,58 @@ import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants'; import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import CiVariablePopover from './ci_variable_popover.vue';
export default { export default {
modalId: ADD_CI_VARIABLE_MODAL_ID, modalId: ADD_CI_VARIABLE_MODAL_ID,
trueIcon: 'mobile-issue-close',
falseIcon: 'close',
iconSize: 16,
fields: [ fields: [
{ {
key: 'variable_type', key: 'variable_type',
label: s__('CiVariables|Type'), label: s__('CiVariables|Type'),
customStyle: { width: '70px' },
}, },
{ {
key: 'key', key: 'key',
label: s__('CiVariables|Key'), label: s__('CiVariables|Key'),
tdClass: 'text-plain',
sortable: true,
customStyle: { width: '40%' },
}, },
{ {
key: 'value', key: 'value',
label: s__('CiVariables|Value'), label: s__('CiVariables|Value'),
tdClass: 'qa-ci-variable-input-value', tdClass: 'qa-ci-variable-input-value',
customStyle: { width: '40%' },
}, },
{ {
key: 'protected', key: 'protected',
label: s__('CiVariables|Protected'), label: s__('CiVariables|Protected'),
customStyle: { width: '100px' },
}, },
{ {
key: 'masked', key: 'masked',
label: s__('CiVariables|Masked'), label: s__('CiVariables|Masked'),
customStyle: { width: '100px' },
}, },
{ {
key: 'environment_scope', key: 'environment_scope',
label: s__('CiVariables|Environment Scope'), label: s__('CiVariables|Environments'),
customStyle: { width: '20%' },
}, },
{ {
key: 'actions', key: 'actions',
label: '', label: '',
customStyle: { width: '35px' },
}, },
], ],
components: { components: {
GlTable, GlTable,
GlButton, GlButton,
GlIcon, GlIcon,
CiVariablePopover,
}, },
directives: { directives: {
GlModalDirective, GlModalDirective,
...@@ -64,7 +78,7 @@ export default { ...@@ -64,7 +78,7 @@ export default {
this.fetchVariables(); this.fetchVariables();
}, },
methods: { methods: {
...mapActions(['fetchVariables', 'deleteVariable', 'toggleValues', 'editVariable']), ...mapActions(['fetchVariables', 'toggleValues', 'editVariable']),
}, },
}; };
</script> </script>
...@@ -74,42 +88,82 @@ export default { ...@@ -74,42 +88,82 @@ export default {
<gl-table <gl-table
:fields="fields" :fields="fields"
:items="variables" :items="variables"
responsive
show-empty
tbody-tr-class="js-ci-variable-row" tbody-tr-class="js-ci-variable-row"
sort-by="key"
sort-direction="asc"
stacked="lg"
fixed
show-empty
sort-icon-left
no-sort-reset
> >
<template #cell(value)="data"> <template #table-colgroup="scope">
<span v-if="valuesHidden">*****************</span> <col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
<span v-else>{{ data.value }}</span> </template>
<template #cell(key)="{ item }">
<div class="d-flex truncated-container">
<span :id="`ci-variable-key-${item.id}`" class="d-inline-block mw-100 text-truncate">{{
item.key
}}</span>
<ci-variable-popover
:target="`ci-variable-key-${item.id}`"
:value="item.key"
:tooltip-text="__('Copy key')"
/>
</div>
</template>
<template #cell(value)="{ item }">
<span v-if="valuesHidden">*********************</span>
<div v-else class="d-flex truncated-container">
<span :id="`ci-variable-value-${item.id}`" class="d-inline-block mw-100 text-truncate">{{
item.value
}}</span>
<ci-variable-popover
:target="`ci-variable-value-${item.id}`"
:value="item.value"
:tooltip-text="__('Copy value')"
/>
</div>
</template>
<template #cell(protected)="{ item }">
<gl-icon v-if="item.protected" :size="$options.iconSize" :name="$options.trueIcon" />
<gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
</template> </template>
<template #cell(actions)="data"> <template #cell(masked)="{ item }">
<gl-icon v-if="item.masked" :size="$options.iconSize" :name="$options.trueIcon" />
<gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
</template>
<template #cell(environment_scope)="{ item }">
<div class="d-flex truncated-container">
<span :id="`ci-variable-env-${item.id}`" class="d-inline-block mw-100 text-truncate">{{
item.environment_scope
}}</span>
<ci-variable-popover
:target="`ci-variable-env-${item.id}`"
:value="item.environment_scope"
:tooltip-text="__('Copy environment')"
/>
</div>
</template>
<template #cell(actions)="{ item }">
<gl-button <gl-button
ref="edit-ci-variable" ref="edit-ci-variable"
v-gl-modal-directive="$options.modalId" v-gl-modal-directive="$options.modalId"
@click="editVariable(data.item)" @click="editVariable(item)"
> >
<gl-icon name="pencil" /> <gl-icon :size="$options.iconSize" name="pencil" />
</gl-button>
<gl-button
ref="delete-ci-variable"
category="secondary"
variant="danger"
@click="deleteVariable(data.item)"
>
<gl-icon name="remove" />
</gl-button> </gl-button>
</template> </template>
<template #empty> <template #empty>
<p ref="empty-variables" class="settings-message text-center empty-variables"> <p ref="empty-variables" class="text-center empty-variables text-plain">
{{ {{ __('There are no variables yet.') }}
__(
'There are currently no variables, add a variable with the Add Variable button below.',
)
}}
</p> </p>
</template> </template>
</gl-table> </gl-table>
<div class="ci-variable-actions d-flex justify-content-end"> <div
class="ci-variable-actions d-flex justify-content-end"
:class="{ 'justify-content-center': !tableIsNotEmpty }"
>
<gl-button <gl-button
v-if="tableIsNotEmpty" v-if="tableIsNotEmpty"
ref="secret-value-reveal-button" ref="secret-value-reveal-button"
... ...
......
// eslint-disable-next-line import/prefer-default-export import { __ } from '~/locale';
// eslint-disable import/prefer-default-export
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable'; export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const displayText = {
variableText: __('Var'),
fileText: __('File'),
allEnvironmentsText: __('All'),
};
export const types = {
variableType: 'env_var',
fileType: 'file',
allEnvironmentsType: '*',
};
import * as types from './mutation_types'; import * as types from './mutation_types';
import { __ } from '~/locale'; import { displayText } from '../constants';
export default { export default {
[types.REQUEST_VARIABLES](state) { [types.REQUEST_VARIABLES](state) {
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
[types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments) { [types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments) {
state.isLoading = false; state.isLoading = false;
state.environments = environments; state.environments = environments;
state.environments.unshift(__('All environments')); state.environments.unshift(displayText.allEnvironmentsText);
}, },
[types.VARIABLE_BEING_EDITED](state, variable) { [types.VARIABLE_BEING_EDITED](state, variable) {
...@@ -70,12 +70,12 @@ export default { ...@@ -70,12 +70,12 @@ export default {
[types.CLEAR_MODAL](state) { [types.CLEAR_MODAL](state) {
state.variable = { state.variable = {
variable_type: __('Variable'), variable_type: displayText.variableText,
key: '', key: '',
secret_value: '', secret_value: '',
protected: false, protected: false,
masked: false, masked: false,
environment_scope: __('All environments'), environment_scope: displayText.allEnvironmentsText,
}; };
}, },
... ...
......
import { __ } from '~/locale'; import { displayText } from '../constants';
export default () => ({ export default () => ({
endpoint: null, endpoint: null,
...@@ -8,17 +8,17 @@ export default () => ({ ...@@ -8,17 +8,17 @@ export default () => ({
isLoading: false, isLoading: false,
isDeleting: false, isDeleting: false,
variable: { variable: {
variable_type: __('Variable'), variable_type: displayText.variableText,
key: '', key: '',
secret_value: '', secret_value: '',
protected: false, protected: false,
masked: false, masked: false,
environment_scope: __('All environments'), environment_scope: displayText.allEnvironmentsText,
}, },
variables: null, variables: null,
valuesHidden: true, valuesHidden: true,
error: null, error: null,
environments: [], environments: [],
typeOptions: [__('Variable'), __('File')], typeOptions: [displayText.variableText, displayText.fileText],
variableBeingEdited: null, variableBeingEdited: null,
}); });
import { __ } from '~/locale';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { displayText, types } from '../constants';
const variableType = 'env_var'; const variableTypeHandler = type =>
const fileType = 'file'; type === displayText.variableText ? types.variableType : types.fileType;
const variableTypeHandler = type => (type === 'Variable' ? variableType : fileType);
export const prepareDataForDisplay = variables => { export const prepareDataForDisplay = variables => {
const variablesToDisplay = []; const variablesToDisplay = [];
variables.forEach(variable => { variables.forEach(variable => {
const variableCopy = variable; const variableCopy = variable;
if (variableCopy.variable_type === variableType) { if (variableCopy.variable_type === types.variableType) {
variableCopy.variable_type = __('Variable'); variableCopy.variable_type = displayText.variableText;
} else { } else {
variableCopy.variable_type = __('File'); variableCopy.variable_type = displayText.fileText;
} }
variableCopy.secret_value = variableCopy.value;
if (variableCopy.environment_scope === '*') { if (variableCopy.environment_scope === types.allEnvironmentsType) {
variableCopy.environment_scope = __('All environments'); variableCopy.environment_scope = displayText.allEnvironmentsText;
} }
variablesToDisplay.push(variableCopy); variablesToDisplay.push(variableCopy);
}); });
...@@ -29,9 +28,8 @@ export const prepareDataForApi = (variable, destroy = false) => { ...@@ -29,9 +28,8 @@ export const prepareDataForApi = (variable, destroy = false) => {
variableCopy.protected = variableCopy.protected.toString(); variableCopy.protected = variableCopy.protected.toString();
variableCopy.masked = variableCopy.masked.toString(); variableCopy.masked = variableCopy.masked.toString();
variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type); variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type);
if (variableCopy.environment_scope === displayText.allEnvironmentsText) {
if (variableCopy.environment_scope === __('All environments')) { variableCopy.environment_scope = types.allEnvironmentsType;
variableCopy.environment_scope = __('*');
} }
if (destroy) { if (destroy) {
... ...
......
...@@ -66,12 +66,12 @@ export default { ...@@ -66,12 +66,12 @@ export default {
individualChartsData() { individualChartsData() {
const maxNumberOfIndividualContributorsCharts = 100; const maxNumberOfIndividualContributorsCharts = 100;
return Object.keys(this.parsedData.byAuthor) return Object.keys(this.parsedData.byAuthorEmail)
.map(name => { .map(email => {
const author = this.parsedData.byAuthor[name]; const author = this.parsedData.byAuthorEmail[email];
return { return {
name, name: author.name,
email: author.email, email,
commits: author.commits, commits: author.commits,
dates: [ dates: [
{ {
... ...
......
export const showChart = state => Boolean(!state.loading && state.chartData); export const showChart = state => Boolean(!state.loading && state.chartData);
export const parsedData = state => { export const parsedData = state => {
const byAuthor = {}; const byAuthorEmail = {};
const total = {}; const total = {};
state.chartData.forEach(({ date, author_name, author_email }) => { state.chartData.forEach(({ date, author_name, author_email }) => {
total[date] = total[date] ? total[date] + 1 : 1; total[date] = total[date] ? total[date] + 1 : 1;
const authorData = byAuthor[author_name]; const authorData = byAuthorEmail[author_email];
if (!authorData) { if (!authorData) {
byAuthor[author_name] = { byAuthorEmail[author_email] = {
email: author_email.toLowerCase(), name: author_name,
commits: 1, commits: 1,
dates: { dates: {
[date]: 1, [date]: 1,
...@@ -25,7 +25,7 @@ export const parsedData = state => { ...@@ -25,7 +25,7 @@ export const parsedData = state => {
return { return {
total, total,
byAuthor, byAuthorEmail,
}; };
}; };
... ...
......
...@@ -11,9 +11,10 @@ const textBuilder = results => { ...@@ -11,9 +11,10 @@ const textBuilder = results => {
const { failed, errored, resolved, total } = results; const { failed, errored, resolved, total } = results;
const failedOrErrored = (failed || 0) + (errored || 0); const failedOrErrored = (failed || 0) + (errored || 0);
const failedString = failedOrErrored const failedString = failed ? n__('%d failed', '%d failed', failed) : null;
? n__('%d failed/error test result', '%d failed/error test results', failedOrErrored) const erroredString = errored ? n__('%d error', '%d errors', errored) : null;
: null; const combinedString =
failed && errored ? `${failedString}, ${erroredString}` : failedString || erroredString;
const resolvedString = resolved const resolvedString = resolved
? n__('%d fixed test result', '%d fixed test results', resolved) ? n__('%d fixed test result', '%d fixed test results', resolved)
: null; : null;
...@@ -23,12 +24,12 @@ const textBuilder = results => { ...@@ -23,12 +24,12 @@ const textBuilder = results => {
if (failedOrErrored) { if (failedOrErrored) {
if (resolved) { if (resolved) {
resultsString = sprintf(s__('Reports|%{failedString} and %{resolvedString}'), { resultsString = sprintf(s__('Reports|%{combinedString} and %{resolvedString}'), {
failedString, combinedString,
resolvedString, resolvedString,
}); });
} else { } else {
resultsString = failedString; resultsString = combinedString;
} }
} else if (resolved) { } else if (resolved) {
resultsString = resolvedString; resultsString = resolvedString;
... ...
......
mutation ($projectPath: ID!, $iid: String!, $healthStatus: HealthStatus) {
updateIssue(input: { projectPath: $projectPath, iid: $iid, healthStatus: $healthStatus}) {
issue {
healthStatus
}
}
}
...@@ -18,7 +18,7 @@ export default class SidebarService { ...@@ -18,7 +18,7 @@ export default class SidebarService {
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint; this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint; this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
this.fullPath = endpointMap.fullPath; this.fullPath = endpointMap.fullPath;
this.id = endpointMap.id; this.iid = endpointMap.iid;
SidebarService.singleton = this; SidebarService.singleton = this;
} }
...@@ -37,7 +37,7 @@ export default class SidebarService { ...@@ -37,7 +37,7 @@ export default class SidebarService {
: sidebarDetailsQuery, : sidebarDetailsQuery,
variables: { variables: {
fullPath: this.fullPath, fullPath: this.fullPath,
iid: this.id.toString(), iid: this.iid.toString(),
}, },
}), }),
]); ]);
...@@ -47,6 +47,17 @@ export default class SidebarService { ...@@ -47,6 +47,17 @@ export default class SidebarService {
return axios.put(this.endpoint, { [key]: data }); return axios.put(this.endpoint, { [key]: data });
} }
updateWithGraphQl(mutation, variables) {
return gqClient.mutate({
mutation,
variables: {
...variables,
projectPath: this.fullPath,
iid: this.iid.toString(),
},
});
}
getProjectsAutocomplete(searchTerm) { getProjectsAutocomplete(searchTerm) {
return axios.get(this.projectsAutocompleteEndpoint, { return axios.get(this.projectsAutocompleteEndpoint, {
params: { params: {
... ...
......
...@@ -20,7 +20,7 @@ export default class SidebarMediator { ...@@ -20,7 +20,7 @@ export default class SidebarMediator {
moveIssueEndpoint: options.moveIssueEndpoint, moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
fullPath: options.fullPath, fullPath: options.fullPath,
id: options.id, iid: options.iid,
}); });
SidebarMediator.singleton = this; SidebarMediator.singleton = this;
} }
... ...
......
...@@ -376,8 +376,29 @@ ...@@ -376,8 +376,29 @@
} }
.ci-variable-table { .ci-variable-table {
table tr th { table {
thead {
border-bottom: 1px solid $white-normal;
}
tr {
td,
th {
padding-left: 0;
}
th {
background-color: transparent; background-color: transparent;
font-weight: $gl-font-weight-bold;
border: 0; border: 0;
} }
} }
}
@media(max-width: map-get($grid-breakpoints, lg)-1) {
.truncated-container {
justify-content: flex-end;
}
}
}
...@@ -463,7 +463,7 @@ module IssuablesHelper ...@@ -463,7 +463,7 @@ module IssuablesHelper
currentUser: issuable[:current_user], currentUser: issuable[:current_user],
rootPath: root_path, rootPath: root_path,
fullPath: issuable[:project_full_path], fullPath: issuable[:project_full_path],
id: issuable[:id], iid: issuable[:iid],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours
} }
end end
... ...
......
...@@ -7,7 +7,7 @@ module Ci ...@@ -7,7 +7,7 @@ module Ci
include Ci::Metadatable include Ci::Metadatable
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include HasRef include Ci::HasRef
InvalidBridgeTypeError = Class.new(StandardError) InvalidBridgeTypeError = Class.new(StandardError)
InvalidTransitionError = Class.new(StandardError) InvalidTransitionError = Class.new(StandardError)
... ...
......
...@@ -10,7 +10,7 @@ module Ci ...@@ -10,7 +10,7 @@ module Ci
include ObjectStorage::BackgroundMove include ObjectStorage::BackgroundMove
include Presentable include Presentable
include Importable include Importable
include HasRef include Ci::HasRef
include IgnorableColumns include IgnorableColumns
BuildArchivedError = Class.new(StandardError) BuildArchivedError = Class.new(StandardError)
... ...
......
...@@ -11,7 +11,7 @@ module Ci ...@@ -11,7 +11,7 @@ module Ci
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include AtomicInternalId include AtomicInternalId
include EnumWithNil include EnumWithNil
include HasRef include Ci::HasRef
include ShaAttribute include ShaAttribute
include FromUnion include FromUnion
include UpdatedAtFilterable include UpdatedAtFilterable
... ...
......
# frozen_string_literal: true
##
# We will disable `ref` and `sha` attributes in `Ci::Build` in the future
# and remove this module in favor of Ci::PipelineDelegator.
module Ci
module HasRef
extend ActiveSupport::Concern
def branch?
!tag? && !merge_request?
end
def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + ref.to_s
end
end
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
# * Lowercased
# * Anything not matching [a-z0-9-] is replaced with a -
# * Maximum length is 63 bytes
# * First/Last Character is not a hyphen
def ref_slug
Gitlab::Utils.slugify(ref.to_s)
end
end
end