summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-08 09:09:43 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-08 09:09:43 +0000
commitf5050253469fc0961c02deec0e698ad62bdd9de5 (patch)
tree30bbd8f8b556fd5b730f0123921138ee1d6bdaa2 /app
parentf6cdec670b9b757fc2225a2c6627ab79765e5b8a (diff)
downloadgitlab-ce-f5050253469fc0961c02deec0e698ad62bdd9de5.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ide/components/external_link.vue34
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue40
-rw-r--r--app/assets/javascripts/logs/stores/actions.js35
-rw-r--r--app/assets/javascripts/logs/stores/mutation_types.js6
-rw-r--r--app/assets/javascripts/logs/stores/mutations.js15
-rw-r--r--app/assets/javascripts/logs/stores/state.js5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue24
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js25
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js8
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js12
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue24
-rw-r--r--app/assets/stylesheets/framework/common.scss3
-rw-r--r--app/assets/stylesheets/framework/mixins.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/builds.scss51
-rw-r--r--app/assets/stylesheets/pages/environment_logs.scss58
-rw-r--r--app/assets/stylesheets/pages/tree.scss6
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/helpers/application_helper.rb1
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/users_statistics.rb33
-rw-r--r--app/views/admin/application_settings/_registry.html.haml9
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/dashboard/stats.html.haml75
27 files changed, 344 insertions, 189 deletions
diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue
deleted file mode 100644
index 558da9b706e..00000000000
--- a/app/assets/javascripts/ide/components/external_link.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<script>
-import Icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- Icon,
- },
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- showButtons() {
- return this.file.permalink;
- },
- },
-};
-</script>
-
-<template>
- <div v-if="showButtons" class="pull-right ide-btn-group">
- <a
- :href="file.permalink"
- :title="s__('IDE|Open in file view')"
- target="_blank"
- rel="noopener noreferrer"
- >
- <span class="vertical-align-middle">{{ __('Open in file view') }}</span>
- <icon :size="16" name="external-link" class="vertical-align-middle space-right" />
- </a>
- </div>
-</template>
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 70b3af8dc75..487b4f30b5b 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -56,7 +56,6 @@ export default {
required: true,
},
},
- traceHeight: 600,
data() {
return {
isElasticStackCalloutDismissed: false,
@@ -94,6 +93,9 @@ export default {
'showEnvironment',
'fetchEnvironments',
'fetchMoreLogsPrepend',
+ 'dismissRequestEnvironmentsError',
+ 'dismissInvalidTimeRangeWarning',
+ 'dismissRequestLogsError',
]),
isCurrentEnvironment(envName) {
@@ -115,7 +117,7 @@ export default {
};
</script>
<template>
- <div class="environment-logs-viewer mt-3">
+ <div class="environment-logs-viewer d-flex flex-column py-3">
<gl-alert
v-if="shouldShowElasticStackCallout"
class="mb-3 js-elasticsearch-alert"
@@ -132,6 +134,31 @@ export default {
</strong>
</a>
</gl-alert>
+ <gl-alert
+ v-if="environments.fetchError"
+ class="mb-3"
+ variant="danger"
+ @dismiss="dismissRequestEnvironmentsError"
+ >
+ {{ s__('Metrics|There was an error fetching the environments data, please try again') }}
+ </gl-alert>
+ <gl-alert
+ v-if="timeRange.invalidWarning"
+ class="mb-3"
+ variant="warning"
+ @dismiss="dismissInvalidTimeRangeWarning"
+ >
+ {{ s__('Metrics|Invalid time range, please verify.') }}
+ </gl-alert>
+ <gl-alert
+ v-if="logs.fetchError"
+ class="mb-3"
+ variant="danger"
+ @dismiss="dismissRequestLogsError"
+ >
+ {{ s__('Environments|There was an error fetching the logs. Please try again.') }}
+ </gl-alert>
+
<div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2">
<div class="flex-grow-0">
<gl-dropdown
@@ -183,16 +210,15 @@ export default {
<gl-infinite-scroll
ref="infiniteScroll"
- class="log-lines"
- :style="{ height: `${$options.traceHeight}px` }"
- :max-list-height="$options.traceHeight"
+ class="log-lines overflow-auto flex-grow-1 min-height-0"
:fetched-items="logs.lines.length"
@topReached="topReached"
@scroll="scroll"
>
<template #items>
<pre
- class="build-trace js-log-trace"
+ ref="logTrace"
+ class="build-trace"
><code class="bash js-build-output"><div v-if="showLoader" class="build-loader-animation js-build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
@@ -205,7 +231,7 @@ export default {
></template>
</gl-infinite-scroll>
- <div ref="logFooter" class="log-footer py-2 px-3">
+ <div ref="logFooter" class="py-2 px-3 text-white bg-secondary-900">
<gl-sprintf :message="s__('Environments|Logs from %{start} to %{end}.')">
<template #start>{{ timeRange.current.start | formatDate }}</template>
<template #end>{{ timeRange.current.end | formatDate }}</template>
diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js
index 1e71b2c7314..be847108a49 100644
--- a/app/assets/javascripts/logs/stores/actions.js
+++ b/app/assets/javascripts/logs/stores/actions.js
@@ -1,20 +1,10 @@
import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
-import { s__ } from '~/locale';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import * as types from './mutation_types';
-const flashTimeRangeWarning = () => {
- flash(s__('Metrics|Invalid time range, please verify.'), 'warning');
-};
-
-const flashLogsError = () => {
- flash(s__('Metrics|There was an error fetching the logs, please try again'));
-};
-
const requestUntilData = (url, params) =>
backOff((next, stop) => {
axios
@@ -31,7 +21,7 @@ const requestUntilData = (url, params) =>
});
});
-const requestLogsUntilData = state => {
+const requestLogsUntilData = ({ commit, state }) => {
const params = {};
const { logs_api_path } = state.environments.options.find(
({ name }) => name === state.environments.current,
@@ -49,7 +39,7 @@ const requestLogsUntilData = state => {
params.start_time = start;
params.end_time = end;
} catch {
- flashTimeRangeWarning();
+ commit(types.SHOW_TIME_RANGE_INVALID_WARNING);
}
}
if (state.logs.cursor) {
@@ -101,26 +91,22 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
})
.catch(() => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_ERROR);
- flash(s__('Metrics|There was an error fetching the environments data, please try again'));
});
};
export const fetchLogs = ({ commit, state }) => {
commit(types.REQUEST_LOGS_DATA);
- return requestLogsUntilData(state)
+ return requestLogsUntilData({ commit, state })
.then(({ data }) => {
const { pod_name, pods, logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
-
commit(types.SET_CURRENT_POD_NAME, pod_name);
-
commit(types.RECEIVE_PODS_DATA_SUCCESS, pods);
})
.catch(() => {
commit(types.RECEIVE_PODS_DATA_ERROR);
commit(types.RECEIVE_LOGS_DATA_ERROR);
- flashLogsError();
});
};
@@ -132,16 +118,27 @@ export const fetchMoreLogsPrepend = ({ commit, state }) => {
commit(types.REQUEST_LOGS_DATA_PREPEND);
- return requestLogsUntilData(state)
+ return requestLogsUntilData({ commit, state })
.then(({ data }) => {
const { logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, { logs, cursor });
})
.catch(() => {
commit(types.RECEIVE_LOGS_DATA_PREPEND_ERROR);
- flashLogsError();
});
};
+export const dismissRequestEnvironmentsError = ({ commit }) => {
+ commit(types.HIDE_REQUEST_ENVIRONMENTS_ERROR);
+};
+
+export const dismissRequestLogsError = ({ commit }) => {
+ commit(types.HIDE_REQUEST_LOGS_ERROR);
+};
+
+export const dismissInvalidTimeRangeWarning = ({ commit }) => {
+ commit(types.HIDE_TIME_RANGE_INVALID_WARNING);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/logs/stores/mutation_types.js b/app/assets/javascripts/logs/stores/mutation_types.js
index 7e7771a9df8..c1cc7eca52e 100644
--- a/app/assets/javascripts/logs/stores/mutation_types.js
+++ b/app/assets/javascripts/logs/stores/mutation_types.js
@@ -1,11 +1,16 @@
export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT';
export const SET_SEARCH = 'SET_SEARCH';
+
export const SET_TIME_RANGE = 'SET_TIME_RANGE';
+export const SHOW_TIME_RANGE_INVALID_WARNING = 'SHOW_TIME_RANGE_INVALID_WARNING';
+export const HIDE_TIME_RANGE_INVALID_WARNING = 'HIDE_TIME_RANGE_INVALID_WARNING';
+
export const SET_CURRENT_POD_NAME = 'SET_CURRENT_POD_NAME';
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_ERROR = 'RECEIVE_ENVIRONMENTS_DATA_ERROR';
+export const HIDE_REQUEST_ENVIRONMENTS_ERROR = 'HIDE_REQUEST_ENVIRONMENTS_ERROR';
export const REQUEST_LOGS_DATA = 'REQUEST_LOGS_DATA';
export const RECEIVE_LOGS_DATA_SUCCESS = 'RECEIVE_LOGS_DATA_SUCCESS';
@@ -13,6 +18,7 @@ export const RECEIVE_LOGS_DATA_ERROR = 'RECEIVE_LOGS_DATA_ERROR';
export const REQUEST_LOGS_DATA_PREPEND = 'REQUEST_LOGS_DATA_PREPEND';
export const RECEIVE_LOGS_DATA_PREPEND_SUCCESS = 'RECEIVE_LOGS_DATA_PREPEND_SUCCESS';
export const RECEIVE_LOGS_DATA_PREPEND_ERROR = 'RECEIVE_LOGS_DATA_PREPEND_ERROR';
+export const HIDE_REQUEST_LOGS_ERROR = 'HIDE_REQUEST_LOGS_ERROR';
export const RECEIVE_PODS_DATA_SUCCESS = 'RECEIVE_PODS_DATA_SUCCESS';
export const RECEIVE_PODS_DATA_ERROR = 'RECEIVE_PODS_DATA_ERROR';
diff --git a/app/assets/javascripts/logs/stores/mutations.js b/app/assets/javascripts/logs/stores/mutations.js
index d77c2894a05..5e1c794c3a9 100644
--- a/app/assets/javascripts/logs/stores/mutations.js
+++ b/app/assets/javascripts/logs/stores/mutations.js
@@ -18,6 +18,12 @@ export default {
state.timeRange.selected = timeRange;
state.timeRange.current = convertToFixedRange(timeRange);
},
+ [types.SHOW_TIME_RANGE_INVALID_WARNING](state) {
+ state.timeRange.invalidWarning = true;
+ },
+ [types.HIDE_TIME_RANGE_INVALID_WARNING](state) {
+ state.timeRange.invalidWarning = false;
+ },
// Environments Data
[types.SET_PROJECT_ENVIRONMENT](state, environmentName) {
@@ -38,6 +44,10 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_ERROR](state) {
state.environments.options = [];
state.environments.isLoading = false;
+ state.environments.fetchError = true;
+ },
+ [types.HIDE_REQUEST_ENVIRONMENTS_ERROR](state) {
+ state.environments.fetchError = false;
},
// Logs data
@@ -63,6 +73,7 @@ export default {
[types.RECEIVE_LOGS_DATA_ERROR](state) {
state.logs.lines = [];
state.logs.isLoading = false;
+ state.logs.fetchError = true;
},
[types.REQUEST_LOGS_DATA_PREPEND](state) {
@@ -80,6 +91,10 @@ export default {
},
[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state) {
state.logs.isLoading = false;
+ state.logs.fetchError = true;
+ },
+ [types.HIDE_REQUEST_LOGS_ERROR](state) {
+ state.logs.fetchError = false;
},
// Pods data
diff --git a/app/assets/javascripts/logs/stores/state.js b/app/assets/javascripts/logs/stores/state.js
index 2c8f47294cc..11185c9ccf1 100644
--- a/app/assets/javascripts/logs/stores/state.js
+++ b/app/assets/javascripts/logs/stores/state.js
@@ -16,6 +16,8 @@ export default () => ({
selected: defaultTimeRange,
// Current time range, must be fixed
current: convertToFixedRange(defaultTimeRange),
+
+ invalidWarning: false,
},
/**
@@ -25,6 +27,7 @@ export default () => ({
options: [],
isLoading: false,
current: null,
+ fetchError: false,
},
/**
@@ -39,6 +42,8 @@ export default () => ({
*/
cursor: null,
isComplete: false,
+
+ fetchError: false,
},
/**
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 6a836adba01..ef3f4d0e3f6 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -99,7 +99,17 @@ export default {
downstreamNode.classList.contains('child-pipeline') ? 15 : 30,
);
- this.$emit('onClickTriggered', this.pipeline, pipeline);
+ /**
+ * If the expanded trigger is defined and the id is different than the
+ * pipeline we clicked, then it means we clicked on a sibling downstream link
+ * and we want to reset the pipeline store. Triggering the reset without
+ * this condition would mean not allowing downstreams of downstreams to expand
+ */
+ if (this.expandedTriggered?.id !== pipeline.id) {
+ this.$emit('onResetTriggered', this.pipeline, pipeline);
+ }
+
+ this.$emit('onClickTriggered', pipeline);
},
calculateMarginTop(downstreamNode, pixelDiff) {
return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
@@ -136,9 +146,7 @@ export default {
:pipeline="expandedTriggeredBy"
:is-linked-pipeline="true"
:mediator="mediator"
- @onClickTriggeredBy="
- (parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline)
- "
+ @onClickTriggeredBy="clickTriggeredByPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
@@ -148,9 +156,7 @@ export default {
:column-title="__('Upstream')"
:project-id="pipelineProjectId"
graph-position="left"
- @linkedPipelineClick="
- linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline)
- "
+ @linkedPipelineClick="$emit('onClickTriggeredBy', $event)"
/>
<ul
@@ -197,9 +203,7 @@ export default {
:is-linked-pipeline="true"
:style="{ 'margin-top': downstreamMarginTop }"
:mediator="mediator"
- @onClickTriggered="
- (parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
- "
+ @onClickTriggered="clickTriggeredPipeline"
@refreshPipelineGraph="requestRefreshPipelineGraph"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
index 1d9366f26df..f987c8f1dd4 100644
--- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
@@ -27,9 +27,9 @@ export default {
* @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
* @param {Object} pipeline The clicked pipeline
*/
- clickPipeline(parentPipeline, pipeline, openMethod, closeMethod) {
+ clickPipeline(pipeline, openMethod, closeMethod) {
if (!pipeline.isExpanded) {
- this.mediator.store[openMethod](parentPipeline, pipeline);
+ this.mediator.store[openMethod](pipeline);
this.mediator.store.toggleLoading(pipeline);
this.mediator.poll.stop();
@@ -41,21 +41,14 @@ export default {
this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
}
},
- clickTriggeredByPipeline(parentPipeline, pipeline) {
- this.clickPipeline(
- parentPipeline,
- pipeline,
- 'openTriggeredByPipeline',
- 'closeTriggeredByPipeline',
- );
+ resetTriggeredPipelines(parentPipeline, pipeline) {
+ this.mediator.store.resetTriggeredPipelines(parentPipeline, pipeline);
},
- clickTriggeredPipeline(parentPipeline, pipeline) {
- this.clickPipeline(
- parentPipeline,
- pipeline,
- 'openTriggeredPipeline',
- 'closeTriggeredPipeline',
- );
+ clickTriggeredByPipeline(pipeline) {
+ this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
+ },
+ clickTriggeredPipeline(pipeline) {
+ this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
},
requestRefreshPipelineGraph() {
// When an action is clicked
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index c901971be50..d76425c96b7 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -42,10 +42,10 @@ export default () => {
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
- onClickTriggeredBy: (parentPipeline, pipeline) =>
- this.clickTriggeredByPipeline(parentPipeline, pipeline),
- onClickTriggered: (parentPipeline, pipeline) =>
- this.clickTriggeredPipeline(parentPipeline, pipeline),
+ onResetTriggered: (parentPipeline, pipeline) =>
+ this.resetTriggeredPipelines(parentPipeline, pipeline),
+ onClickTriggeredBy: pipeline => this.clickTriggeredByPipeline(pipeline),
+ onClickTriggered: pipeline => this.clickTriggeredPipeline(pipeline),
},
});
},
diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js
index 69e3579a3c7..1ef73760e02 100644
--- a/app/assets/javascripts/pipelines/stores/pipeline_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js
@@ -54,16 +54,24 @@ export default class PipelineStore {
*/
parseTriggeredByPipelines(oldPipeline = {}, newPipeline) {
// keep old value in case it's opened because we're polling
-
Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false);
// add isLoading property
Vue.set(newPipeline, 'isLoading', false);
+ // Because there can only ever be one `triggered_by` for any given pipeline,
+ // the API returns an object for the value instead of an Array. However,
+ // it's easier to deal with an array in the FE so we convert it.
if (newPipeline.triggered_by) {
if (!Array.isArray(newPipeline.triggered_by)) {
Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] });
}
- this.parseTriggeredByPipelines(oldPipeline, newPipeline.triggered_by[0]);
+
+ if (newPipeline.triggered_by?.length > 0) {
+ newPipeline.triggered_by.forEach(el => {
+ const oldTriggeredBy = oldPipeline.triggered_by?.find(element => element.id === el.id);
+ this.parseTriggeredPipelines(oldTriggeredBy, el);
+ });
+ }
}
}
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index a057913fd5a..00ccc49d770 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,10 +1,16 @@
<script>
import { escapeRegExp } from 'lodash';
-import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlBadge,
+ GlLink,
+ GlSkeletonLoading,
+ GlTooltipDirective,
+ GlLoadingIcon,
+ GlIcon,
+} from '@gitlab/ui';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-import { getIconName } from '../../utils/icon';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import getRefMixin from '../../mixins/get_ref';
import getCommit from '../../queries/getCommit.query.graphql';
@@ -14,8 +20,9 @@ export default {
GlLink,
GlSkeletonLoading,
GlLoadingIcon,
+ GlIcon,
TimeagoTooltip,
- Icon,
+ FileIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -95,9 +102,6 @@ export default {
? { path: `/-/tree/${escape(this.ref)}/${escapeFileUrl(this.path)}` }
: null;
},
- iconName() {
- return `fa-${getIconName(this.type, this.path)}`;
- },
isFolder() {
return this.type === 'tree';
},
@@ -123,12 +127,6 @@ export default {
<template>
<tr class="tree-item">
<td class="tree-item-file-name cursor-default position-relative">
- <gl-loading-icon
- v-if="path === loadingPath"
- size="sm"
- inline
- class="d-inline-block align-text-bottom fa-fw"
- />
<component
:is="linkComponent"
ref="link"
@@ -140,27 +138,27 @@ export default {
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
>
- <i
- v-if="path !== loadingPath"
- :aria-label="type"
- role="img"
- :class="iconName"
- class="fa fa-fw mr-1"
- ></i
- ><span class="position-relative">{{ fullPath }}</span>
+ <file-icon
+ :file-name="fullPath"
+ :folder="isFolder"
+ :submodule="isSubmodule"
+ :loading="path === loadingPath"
+ css-classes="position-relative file-icon"
+ class="mr-1 position-relative text-secondary"
+ /><span class="position-relative">{{ fullPath }}</span>
</component>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge>
<template v-if="isSubmodule">
@ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link>
</template>
- <icon
+ <gl-icon
v-if="hasLockLabel"
v-gl-tooltip
:title="commit.lockLabel"
name="lock"
:size="12"
- class="ml-2 vertical-align-middle"
+ class="ml-1"
/>
</td>
<td class="d-none d-sm-table-cell tree-commit cursor-default">
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 952ffa1fa0e..b084ebdf774 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,7 +1,6 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import getIconForFile from './file_icon/file_icon_map';
-import icon from '../../vue_shared/components/icon.vue';
/* This is a re-usable vue component for rendering a svg sprite
icon
@@ -17,8 +16,8 @@ import icon from '../../vue_shared/components/icon.vue';
*/
export default {
components: {
- icon,
GlLoadingIcon,
+ GlIcon,
},
props: {
fileName: {
@@ -31,7 +30,11 @@ export default {
required: false,
default: false,
},
-
+ submodule: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
opened: {
type: Boolean,
required: false,
@@ -58,7 +61,7 @@ export default {
},
computed: {
spriteHref() {
- const iconName = getIconForFile(this.fileName) || 'file';
+ const iconName = this.submodule ? 'folder-git' : getIconForFile(this.fileName) || 'file';
return `${gon.sprite_file_icons}#${iconName}`;
},
folderIconName() {
@@ -73,9 +76,12 @@ export default {
<template>
<span>
<svg v-if="!loading && !folder" :class="[iconSizeClass, cssClasses]">
- <use v-bind="{ 'xlink:href': spriteHref }" />
- </svg>
- <icon v-if="!loading && folder" :name="folderIconName" :size="size" class="folder-icon" />
- <gl-loading-icon v-if="loading" :inline="true" />
+ <use v-bind="{ 'xlink:href': spriteHref }" /></svg
+ ><gl-icon
+ v-if="!loading && folder"
+ :name="folderIconName"
+ :size="size"
+ class="folder-icon"
+ /><gl-loading-icon v-if="loading" :inline="true" />
</span>
</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 211e1e30161..320bd4adaaa 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -474,6 +474,9 @@ img.emoji {
.mw-70p { max-width: 70%; }
.mw-90p { max-width: 90%; }
+// By default flex items don't shrink below their minimum content size.
+// To change this, these clases set a min-width or min-height
+.min-width-0 { min-width: 0; }
.min-height-0 { min-height: 0; }
.svg-w-100 {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 76b12b2405f..52da1b9abfc 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -199,8 +199,8 @@
/*
* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs)
*/
-@mixin build-trace {
- background: $black;
+@mixin build-trace($background: $black) {
+ background: $background;
color: $gray-darkest;
white-space: pre;
overflow-x: auto;
@@ -243,7 +243,7 @@
/*
* Mixin that handles the position of the controls placed on the top bar
*/
-@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') {
+@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: block, $svg-top: 2px) {
display: flex;
font-size: $control-font-size;
justify-content: $flex-direction;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a3c1d8b1709..65efbabaa4f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -641,6 +641,14 @@ $issue-boards-breadcrumbs-height-xs: 63px;
$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
+/*
+ The following heights are used in environment_logs.scss and are used for calculation of the log viewer height.
+*/
+$environment-logs-breadcrumbs-height: 63px;
+$environment-logs-breadcrumbs-height-md: $breadcrumb-min-height;
+
+$environment-logs-difference-xs-up: $header-height + $environment-logs-breadcrumbs-height;
+$environment-logs-difference-md-up: $header-height + $environment-logs-breadcrumbs-height-md;
/*
* Avatar
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index f8b8a7271ce..f50d4bc736e 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -356,54 +356,3 @@
}
}
}
-
-.environment-logs-viewer {
- .build-trace-container {
- position: relative;
- }
-
- .log-lines,
- .gl-infinite-scroll-container {
- // makes scrollbar visible by creating contrast
- background: $black;
- }
-
- .gl-infinite-scroll-legend {
- margin: 0;
- }
-
- .build-trace {
- @include build-trace();
- margin: 0;
- }
-
- .top-bar {
- .date-time-picker-wrapper,
- .dropdown-toggle {
- @include media-breakpoint-up(md) {
- width: 140px;
- }
-
- @include media-breakpoint-up(lg) {
- width: 160px;
- }
- }
-
- .controllers {
- @include build-controllers(16px, flex-end, false, 2);
- }
- }
-
- .btn-refresh svg {
- top: 0;
- }
-
- .build-loader-animation {
- @include build-loader-animation;
- }
-
- .log-footer {
- color: $white-normal;
- background-color: $gray-900;
- }
-}
diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss
new file mode 100644
index 00000000000..81cec14062f
--- /dev/null
+++ b/app/assets/stylesheets/pages/environment_logs.scss
@@ -0,0 +1,58 @@
+.environment-logs-page {
+ .content-wrapper {
+ padding-bottom: 0;
+ }
+}
+
+.environment-logs-viewer {
+ height: calc(100vh - #{$environment-logs-difference-xs-up});
+ min-height: 700px;
+
+ @include media-breakpoint-up(md) {
+ height: calc(100vh - #{$environment-logs-difference-md-up});
+ }
+
+ .with-performance-bar & {
+ height: calc(100vh - #{$environment-logs-difference-xs-up} - #{$performance-bar-height});
+
+ @include media-breakpoint-up(md) {
+ height: calc(100vh - #{$environment-logs-difference-md-up} - #{$performance-bar-height});
+ }
+ }
+
+ .top-bar {
+ .date-time-picker-wrapper,
+ .dropdown-toggle {
+ @include media-breakpoint-up(md) {
+ width: 140px;
+ }
+
+ @include media-breakpoint-up(lg) {
+ width: 160px;
+ }
+ }
+
+ .controllers {
+ @include build-controllers(16px, flex-end, false, 2, inline);
+ }
+ }
+
+ .log-lines,
+ .gl-infinite-scroll-container {
+ // makes scrollbar visible by creating contrast
+ background: $black;
+ height: 100%;
+ }
+
+ .build-trace {
+ @include build-trace($black);
+ }
+
+ .gl-infinite-scroll-legend {
+ margin: 0;
+ }
+
+ .build-loader-animation {
+ @include build-loader-animation;
+ }
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index a03101c66ac..142078588df 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -138,6 +138,12 @@
}
.tree-item {
+ .file-icon,
+ .folder-icon {
+ position: relative;
+ top: 2px;
+ }
+
.link-container {
padding: 0;
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index ae94edac734..cd95105a893 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -16,6 +16,10 @@ class Admin::DashboardController < Admin::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def stats
+ @users_statistics = UsersStatistics.latest
+ end
+
def show_license_breakdown?
false
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 83ecc7753b6..a815b378f8b 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -256,6 +256,7 @@ module ApplicationHelper
def page_class
class_names = []
class_names << 'issue-boards-page' if current_controller?(:boards)
+ class_names << 'environment-logs-page' if current_controller?(:logs)
class_names << 'with-performance-bar' if performance_bar_enabled?
class_names << system_message_class
class_names
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3694d9e2abe..443451cd394 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -176,6 +176,7 @@ module ApplicationSettingsHelper
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
+ :container_expiration_policies_enable_historic_entries,
:container_registry_token_expire_delay,
:default_artifacts_expire_in,
:default_branch_protection,
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 9254f7dd633..c1e44748304 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -142,6 +142,9 @@ class ApplicationSetting < ApplicationRecord
validates :default_artifacts_expire_in, presence: true, duration: true
+ validates :container_expiration_policies_enable_historic_entries,
+ inclusion: { in: [true, false], message: 'must be a boolean value' }
+
validates :container_registry_token_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 418fb18cc91..920ad3286d1 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -42,6 +42,7 @@ module ApplicationSettingImplementation
asset_proxy_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname,
+ container_expiration_policies_enable_historic_entries: false,
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb
index 1a500717efd..37a430015e5 100644
--- a/app/models/users_statistics.rb
+++ b/app/models/users_statistics.rb
@@ -1,16 +1,29 @@
# frozen_string_literal: true
class UsersStatistics < ApplicationRecord
- STATISTICS_NAMES = [
- :without_groups_and_projects,
- :with_highest_role_guest,
- :with_highest_role_reporter,
- :with_highest_role_developer,
- :with_highest_role_maintainer,
- :with_highest_role_owner,
- :bots,
- :blocked
- ].freeze
+ scope :order_created_at_desc, -> { order(created_at: :desc) }
+
+ class << self
+ def latest
+ order_created_at_desc.first
+ end
+ end
+
+ def active
+ [
+ without_groups_and_projects,
+ with_highest_role_guest,
+ with_highest_role_reporter,
+ with_highest_role_developer,
+ with_highest_role_maintainer,
+ with_highest_role_owner,
+ bots
+ ].sum
+ end
+
+ def total
+ active + blocked
+ end
class << self
def create_current_stats!
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index 77623e1495b..0631c024eb8 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -5,5 +5,14 @@
.form-group
= f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-bold'
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
+ .form-group
+ .form-check
+ = f.check_box :container_expiration_policies_enable_historic_entries, class: 'form-check-input'
+ = f.label :container_expiration_policies_enable_historic_entries, class: 'form-check-label' do
+ = _("Enable container expiration and retention policies for projects created earlier than GitLab 12.7.")
+ = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy')
+ .form-text.text-muted
+ = _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.")
+ = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 68f761c75d8..951e5364ad8 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -30,7 +30,7 @@
%hr
.btn-group.d-flex{ role: 'group' }
= link_to 'New user', new_admin_user_path, class: "btn btn-success"
- = render_if_exists 'admin/dashboard/users_statistics'
+ = link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary'
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
diff --git a/app/views/admin/dashboard/stats.html.haml b/app/views/admin/dashboard/stats.html.haml
new file mode 100644
index 00000000000..f7f2c717308
--- /dev/null
+++ b/app/views/admin/dashboard/stats.html.haml
@@ -0,0 +1,75 @@
+- page_title s_('AdminArea|Users statistics')
+
+%h3.my-4
+ = s_('AdminArea|Users statistics')
+%table.table.gl-text-gray-700
+ %tr
+ %td.p-3
+ = s_('AdminArea|Users without a Group and Project')
+ = render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
+ %td.p-3.text-right
+ = @users_statistics&.without_groups_and_projects.to_i
+ %tr
+ %td.p-3
+ = s_('AdminArea|Users with highest role')
+ %strong
+ = s_('AdminArea|Guest')
+ = render_if_exists 'admin/dashboard/included_free_in_license_tooltip'
+ %td.p-3.text-right
+ = @users_statistics&.with_highest_role_guest.to_i
+ %tr
+ %td.p-3
+ = s_('AdminArea|Users with highest role')
+ %strong
+ = s_('AdminArea|Reporter')
+ %td.p-3.text-right
+ = @users_statistics&.with_highest_role_reporter.to_i
+ %tr
+ %td.p-3
+ = s_('AdminArea|Users with highest role')
+ %strong
+ = s_('AdminArea|Developer')
+ %td.p-3.text-right
+ = @users_statistics&.with_highest_role_developer.to_i
+ %tr
+ %td.p-3
+ = s_('AdminArea|Users with highest role')
+ %strong
+ = s_('AdminArea|Maintainer')
+ %td.p-3.text-right
+ = @users_statistics&.with_highest_role_maintainer.to_i
+ %tr
+ %td.p-3
+ = s_('AdminArea|Users with highest role')
+ %strong
+ = s_('AdminArea|Owner')
+ %td.p-3.text-right
+ = @users_statistics&.with_highest_role_owner.to_i
+ %tr
+ %td.p-3
+ = s_('AdminArea|Bots')
+ %td.p-3.text-right
+ = @users_statistics&.bots.to_i
+
+ %tr.bg-gray-light.gl-text-gray-900
+ %td.p-3
+ %strong
+ = s_('AdminArea|Active users')
+ = render_if_exists 'admin/dashboard/billable_users_text'
+ %td.p-3.text-right
+ %strong
+ = @users_statistics&.active.to_i
+ %tr.bg-gray-light.gl-text-gray-900
+ %td.p-3
+ %strong
+ = s_('AdminArea|Blocked users')
+ %td.p-3.text-right
+ %strong
+ = @users_statistics&.blocked.to_i
+ %tr.bg-gray-light.gl-text-gray-900
+ %td.p-3
+ %strong
+ = s_('AdminArea|Total users')
+ %td.p-3.text-right
+ %strong
+ = @users_statistics&.total.to_i