Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
75ee59f7a1
commit
b77fb04678
34 changed files with 600 additions and 378 deletions
|
@ -89,10 +89,9 @@ export default {
|
|||
methods: {
|
||||
...mapActions('environmentLogs', [
|
||||
'setInitData',
|
||||
'setSearch',
|
||||
'showPodLogs',
|
||||
'showEnvironment',
|
||||
'fetchEnvironments',
|
||||
'fetchLogs',
|
||||
'fetchMoreLogsPrepend',
|
||||
'dismissRequestEnvironmentsError',
|
||||
'dismissInvalidTimeRangeWarning',
|
||||
|
@ -191,13 +190,13 @@ export default {
|
|||
<log-advanced-filters
|
||||
v-if="showAdvancedFilters"
|
||||
ref="log-advanced-filters"
|
||||
class="d-md-flex flex-grow-1"
|
||||
class="d-md-flex flex-grow-1 min-width-0"
|
||||
:disabled="environments.isLoading"
|
||||
/>
|
||||
<log-simple-filters
|
||||
v-else
|
||||
ref="log-simple-filters"
|
||||
class="d-md-flex flex-grow-1"
|
||||
class="d-md-flex flex-grow-1 min-width-0"
|
||||
:disabled="environments.isLoading"
|
||||
/>
|
||||
|
||||
|
@ -205,7 +204,7 @@ export default {
|
|||
ref="scrollButtons"
|
||||
class="flex-grow-0 pr-2 mb-2 controllers"
|
||||
:scroll-down-button-disabled="scrollDownButtonDisabled"
|
||||
@refresh="showPodLogs(pods.current)"
|
||||
@refresh="fetchLogs()"
|
||||
@scrollDown="scrollDown"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
<script>
|
||||
import { s__ } from '~/locale';
|
||||
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import {
|
||||
GlIcon,
|
||||
GlDropdown,
|
||||
GlDropdownHeader,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
GlSearchBoxByClick,
|
||||
} from '@gitlab/ui';
|
||||
import { GlFilteredSearch } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
|
||||
import { timeRanges } from '~/vue_shared/constants';
|
||||
import { TOKEN_TYPE_POD_NAME } from '../constants';
|
||||
import TokenWithLoadingState from './tokens/token_with_loading_state.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlDropdown,
|
||||
GlDropdownHeader,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
GlSearchBoxByClick,
|
||||
GlFilteredSearch,
|
||||
DateTimePicker,
|
||||
},
|
||||
props: {
|
||||
|
@ -32,11 +22,10 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
timeRanges,
|
||||
searchQuery: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('environmentLogs', ['timeRange', 'pods']),
|
||||
...mapState('environmentLogs', ['timeRange', 'pods', 'logs']),
|
||||
|
||||
timeRangeModel: {
|
||||
get() {
|
||||
|
@ -46,75 +35,56 @@ export default {
|
|||
this.setTimeRange(val);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Token options.
|
||||
*
|
||||
* Returns null when no pods are present, so suggestions are displayed in the token
|
||||
*/
|
||||
podOptions() {
|
||||
if (this.pods.options.length) {
|
||||
return this.pods.options.map(podName => ({ value: podName, title: podName }));
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
podDropdownText() {
|
||||
return this.pods.current || s__('Environments|All pods');
|
||||
tokens() {
|
||||
return [
|
||||
{
|
||||
icon: 'pod',
|
||||
type: TOKEN_TYPE_POD_NAME,
|
||||
title: s__('Environments|Pod name'),
|
||||
token: TokenWithLoadingState,
|
||||
operators: [{ value: '=', description: __('is'), default: 'true' }],
|
||||
unique: true,
|
||||
options: this.podOptions,
|
||||
loading: this.logs.isLoading,
|
||||
noOptionsText: s__('Environments|No pods to display'),
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('environmentLogs', ['setSearch', 'showPodLogs', 'setTimeRange']),
|
||||
isCurrentPod(podName) {
|
||||
return podName === this.pods.current;
|
||||
...mapActions('environmentLogs', ['showFilteredLogs', 'setTimeRange']),
|
||||
|
||||
filteredSearchSubmit(filters) {
|
||||
this.showFilteredLogs(filters);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-dropdown
|
||||
ref="podsDropdown"
|
||||
:text="podDropdownText"
|
||||
:disabled="disabled"
|
||||
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown"
|
||||
>
|
||||
<gl-dropdown-header class="text-center">
|
||||
{{ s__('Environments|Filter by pod') }}
|
||||
</gl-dropdown-header>
|
||||
|
||||
<gl-dropdown-item v-if="!pods.options.length" disabled>
|
||||
<span ref="noPodsMsg" class="text-muted">
|
||||
{{ s__('Environments|No pods to display') }}
|
||||
</span>
|
||||
</gl-dropdown-item>
|
||||
|
||||
<template v-else>
|
||||
<gl-dropdown-item ref="allPodsOption" key="all-pods" @click="showPodLogs(null)">
|
||||
<div class="d-flex">
|
||||
<gl-icon
|
||||
:class="{ invisible: pods.current !== null }"
|
||||
name="status_success_borderless"
|
||||
<div class="mb-2 pr-2 flex-grow-1 min-width-0">
|
||||
<gl-filtered-search
|
||||
:placeholder="__('Search')"
|
||||
:clear-button-title="__('Clear')"
|
||||
:close-button-title="__('Close')"
|
||||
class="gl-h-32"
|
||||
:disabled="disabled || logs.isLoading"
|
||||
:available-tokens="tokens"
|
||||
@submit="filteredSearchSubmit"
|
||||
/>
|
||||
<div class="flex-grow-1">{{ s__('Environments|All pods') }}</div>
|
||||
</div>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item
|
||||
v-for="podName in pods.options"
|
||||
:key="podName"
|
||||
class="text-nowrap"
|
||||
@click="showPodLogs(podName)"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<gl-icon
|
||||
:class="{ invisible: !isCurrentPod(podName) }"
|
||||
name="status_success_borderless"
|
||||
/>
|
||||
<div class="flex-grow-1">{{ podName }}</div>
|
||||
</div>
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
|
||||
<gl-search-box-by-click
|
||||
ref="searchBox"
|
||||
v-model.trim="searchQuery"
|
||||
:disabled="disabled"
|
||||
:placeholder="s__('Environments|Search')"
|
||||
class="mb-2 pr-2 flex-grow-1"
|
||||
type="search"
|
||||
autofocus
|
||||
@submit="setSearch(searchQuery)"
|
||||
/>
|
||||
|
||||
<date-time-picker
|
||||
ref="dateTimePicker"
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFilteredSearchToken,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-filtered-search-token :config="config" v-bind="{ ...$attrs }" v-on="$listeners">
|
||||
<template #suggestions>
|
||||
<div class="m-1">
|
||||
<gl-loading-icon v-if="config.loading" />
|
||||
<div v-else class="py-1 px-2 text-muted">
|
||||
{{ config.noOptionsText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</gl-filtered-search-token>
|
||||
</template>
|
3
app/assets/javascripts/logs/constants.js
Normal file
3
app/assets/javascripts/logs/constants.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const dateFormatMask = 'UTC:mmm dd HH:MM:ss.l"Z"';
|
||||
|
||||
export const TOKEN_TYPE_POD_NAME = 'TOKEN_TYPE_POD_NAME';
|
|
@ -2,6 +2,7 @@ import { backOff } from '~/lib/utils/common_utils';
|
|||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { convertToFixedRange } from '~/lib/utils/datetime_range';
|
||||
import { TOKEN_TYPE_POD_NAME } from '../constants';
|
||||
|
||||
import * as types from './mutation_types';
|
||||
|
||||
|
@ -49,19 +50,42 @@ const requestLogsUntilData = ({ commit, state }) => {
|
|||
return requestUntilData(logs_api_path, params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts filters emitted by the component, e.g. a filterered-search
|
||||
* to parameters to be applied to the filters of the store
|
||||
* @param {Array} filters - List of strings or objects to filter by.
|
||||
* @returns {Object} - An object with `search` and `podName` keys.
|
||||
*/
|
||||
const filtersToParams = (filters = []) => {
|
||||
// Strings become part of the `search`
|
||||
const search = filters
|
||||
.filter(f => typeof f === 'string')
|
||||
.join(' ')
|
||||
.trim();
|
||||
|
||||
// null podName to show all pods
|
||||
const podName = filters.find(f => f?.type === TOKEN_TYPE_POD_NAME)?.value?.data ?? null;
|
||||
|
||||
return { search, podName };
|
||||
};
|
||||
|
||||
export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => {
|
||||
commit(types.SET_TIME_RANGE, timeRange);
|
||||
commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
|
||||
commit(types.SET_CURRENT_POD_NAME, podName);
|
||||
};
|
||||
|
||||
export const showPodLogs = ({ dispatch, commit }, podName) => {
|
||||
export const showFilteredLogs = ({ dispatch, commit }, filters = []) => {
|
||||
const { podName, search } = filtersToParams(filters);
|
||||
|
||||
commit(types.SET_CURRENT_POD_NAME, podName);
|
||||
commit(types.SET_SEARCH, search);
|
||||
|
||||
dispatch('fetchLogs');
|
||||
};
|
||||
|
||||
export const setSearch = ({ dispatch, commit }, searchQuery) => {
|
||||
commit(types.SET_SEARCH, searchQuery);
|
||||
export const showPodLogs = ({ dispatch, commit }, podName) => {
|
||||
commit(types.SET_CURRENT_POD_NAME, podName);
|
||||
dispatch('fetchLogs');
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
const dateFormatMask = 'UTC:mmm dd HH:MM:ss.l"Z"';
|
||||
import { dateFormatMask } from './constants';
|
||||
|
||||
/**
|
||||
* Returns a time range (`start`, `end`) where `start` is the
|
||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="border-bottom pb-4">
|
||||
<h3>{{ s__('StaticSiteEditor|Success!') }}</h3>
|
||||
<p>
|
||||
{{
|
||||
|
@ -45,35 +45,37 @@ export default {
|
|||
)
|
||||
}}
|
||||
</p>
|
||||
<div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gl-new-button ref="returnToSiteButton" :href="returnUrl">{{
|
||||
s__('StaticSiteEditor|Return to site')
|
||||
}}</gl-new-button>
|
||||
<gl-new-button ref="mergeRequestButton" :href="mergeRequest.url" variant="info">{{
|
||||
s__('StaticSiteEditor|View merge request')
|
||||
}}</gl-new-button>
|
||||
<gl-new-button
|
||||
ref="mergeRequestButton"
|
||||
class="ml-2"
|
||||
:href="mergeRequest.url"
|
||||
variant="success"
|
||||
>{{ s__('StaticSiteEditor|View merge request') }}</gl-new-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<div class="pt-2">
|
||||
<h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4>
|
||||
<ul>
|
||||
<li>
|
||||
{{ s__('StaticSiteEditor|A new branch was created:') }}
|
||||
{{ s__('StaticSiteEditor|You created a new branch:') }}
|
||||
<gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link>
|
||||
</li>
|
||||
<li>
|
||||
{{ s__('StaticSiteEditor|Your changes were committed to it:') }}
|
||||
<gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
|
||||
</li>
|
||||
<li>
|
||||
{{ s__('StaticSiteEditor|A merge request was created:') }}
|
||||
{{ s__('StaticSiteEditor|You created a merge request:') }}
|
||||
<gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{
|
||||
mergeRequest.label
|
||||
}}</gl-link>
|
||||
</li>
|
||||
<li>
|
||||
{{ s__('StaticSiteEditor|You added a commit:') }}
|
||||
<gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -96,8 +96,8 @@
|
|||
}
|
||||
|
||||
.name {
|
||||
background-color: $filter-name-resting-color;
|
||||
color: $filter-name-text-color;
|
||||
background-color: $white-normal;
|
||||
color: $gl-text-color-secondary;
|
||||
border-radius: 2px 0 0 2px;
|
||||
margin-right: 1px;
|
||||
text-transform: capitalize;
|
||||
|
@ -105,7 +105,7 @@
|
|||
|
||||
.operator {
|
||||
background-color: $white-normal;
|
||||
color: $filter-value-text-color;
|
||||
color: $gl-text-color;
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $white-normal;
|
||||
color: $filter-value-text-color;
|
||||
color: $gl-text-color;
|
||||
border-radius: 0 2px 2px 0;
|
||||
margin-right: 5px;
|
||||
padding-right: 8px;
|
||||
|
@ -152,7 +152,7 @@
|
|||
.filtered-search-token .selected,
|
||||
.filtered-search-term .selected {
|
||||
.name {
|
||||
background-color: $filter-name-selected-color;
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.operator {
|
||||
|
|
|
@ -86,13 +86,13 @@
|
|||
line-height: 10px;
|
||||
color: $gl-gray-700;
|
||||
vertical-align: middle;
|
||||
background-color: $kdb-bg;
|
||||
background-color: $gray-50;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: $gl-gray-200 $gl-gray-200 $kdb-border-bottom;
|
||||
border-color: $gray-200 $gray-200 $gray-400;
|
||||
border-image: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 -1px 0 $kdb-shadow inset;
|
||||
box-shadow: 0 -1px 0 $gray-400 inset;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
|
|
@ -485,7 +485,7 @@ $line-removed-dark: #fac5cd;
|
|||
$line-number-old: #f9d7dc;
|
||||
$line-number-new: #ddfbe6;
|
||||
$line-number-select: #fbf2da;
|
||||
$line-target-blue: #f6faff;
|
||||
$line-target-blue: $blue-50;
|
||||
$line-select-yellow: #fcf8e7;
|
||||
$line-select-yellow-dark: #f0e2bd;
|
||||
$dark-diff-match-bg: rgba(255, 255, 255, 0.3);
|
||||
|
@ -698,7 +698,7 @@ $logs-p-color: #333;
|
|||
*/
|
||||
$input-height: 34px;
|
||||
$input-danger-bg: #f2dede;
|
||||
$input-group-addon-bg: #f7f8fa;
|
||||
$input-group-addon-bg: $gray-50;
|
||||
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
|
||||
$gl-field-focus-shadow-error: rgba($red-500, 0.6);
|
||||
$input-short-width: 200px;
|
||||
|
@ -774,9 +774,6 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
|
|||
/*
|
||||
* Typography
|
||||
*/
|
||||
$kdb-bg: #fcfcfc;
|
||||
$kdb-border-bottom: #bbb;
|
||||
$kdb-shadow: #bbb;
|
||||
$body-text-shadow: rgba(255, 255, 255, 0.01);
|
||||
|
||||
/*
|
||||
|
@ -800,20 +797,6 @@ CI variable lists
|
|||
*/
|
||||
$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
|
||||
|
||||
/*
|
||||
Filtered Search
|
||||
*/
|
||||
$filter-name-resting-color: #f8f8f8;
|
||||
$filter-name-text-color: rgba(0, 0, 0, 0.55);
|
||||
$filter-value-text-color: rgba(0, 0, 0, 0.85);
|
||||
$filter-name-selected-color: #ebebeb;
|
||||
$filter-value-selected-color: #d7d7d7;
|
||||
|
||||
/*
|
||||
Animation Functions
|
||||
*/
|
||||
$dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
/*
|
||||
GitLab Plans
|
||||
*/
|
||||
|
|
|
@ -54,6 +54,11 @@
|
|||
|
||||
.mh-50vh { max-height: 50vh; }
|
||||
|
||||
.min-width-0 {
|
||||
// By default flex items don't shrink below their minimum content size. To change this, set the item's min-width
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.font-size-inherit { font-size: inherit; }
|
||||
.gl-w-8 { width: px-to-rem($grid-size); }
|
||||
.gl-w-16 { width: px-to-rem($grid-size * 2); }
|
||||
|
|
|
@ -475,6 +475,16 @@ class Group < Namespace
|
|||
false
|
||||
end
|
||||
|
||||
def wiki_access_level
|
||||
# TODO: Remove this method once we implement group-level features.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/208412
|
||||
if Feature.enabled?(:group_wiki, self)
|
||||
ProjectFeature::ENABLED
|
||||
else
|
||||
ProjectFeature::DISABLED
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_two_factor_requirement
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectPolicy
|
||||
module ClassMethods
|
||||
module CrudPolicyHelpers
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def create_read_update_admin_destroy(name)
|
||||
[
|
||||
:"read_#{name}",
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GroupPolicy < BasePolicy
|
||||
include CrudPolicyHelpers
|
||||
include FindGroupProjects
|
||||
|
||||
desc "Group is public"
|
||||
|
@ -42,15 +43,23 @@ class GroupPolicy < BasePolicy
|
|||
@subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS
|
||||
end
|
||||
|
||||
desc "Group has wiki disabled"
|
||||
condition(:wiki_disabled, score: 32) { !feature_available?(:wiki) }
|
||||
|
||||
rule { public_group }.policy do
|
||||
enable :read_group
|
||||
enable :read_package
|
||||
enable :read_wiki
|
||||
end
|
||||
|
||||
rule { logged_in_viewable }.enable :read_group
|
||||
rule { logged_in_viewable }.policy do
|
||||
enable :read_group
|
||||
enable :read_wiki
|
||||
end
|
||||
|
||||
rule { guest }.policy do
|
||||
enable :read_group
|
||||
enable :read_wiki
|
||||
enable :upload_file
|
||||
end
|
||||
|
||||
|
@ -78,10 +87,12 @@ class GroupPolicy < BasePolicy
|
|||
enable :create_metrics_dashboard_annotation
|
||||
enable :delete_metrics_dashboard_annotation
|
||||
enable :update_metrics_dashboard_annotation
|
||||
enable :create_wiki
|
||||
end
|
||||
|
||||
rule { reporter }.policy do
|
||||
enable :read_container_image
|
||||
enable :download_wiki_code
|
||||
enable :admin_label
|
||||
enable :admin_list
|
||||
enable :admin_issue
|
||||
|
@ -100,6 +111,7 @@ class GroupPolicy < BasePolicy
|
|||
enable :destroy_deploy_token
|
||||
enable :read_deploy_token
|
||||
enable :create_deploy_token
|
||||
enable :admin_wiki
|
||||
end
|
||||
|
||||
rule { owner }.policy do
|
||||
|
@ -145,6 +157,11 @@ class GroupPolicy < BasePolicy
|
|||
|
||||
rule { maintainer & can?(:create_projects) }.enable :transfer_projects
|
||||
|
||||
rule { wiki_disabled }.policy do
|
||||
prevent(*create_read_update_admin_destroy(:wiki))
|
||||
prevent(:download_wiki_code)
|
||||
end
|
||||
|
||||
def access_level
|
||||
return GroupMember::NO_ACCESS if @user.nil?
|
||||
|
||||
|
@ -154,6 +171,21 @@ class GroupPolicy < BasePolicy
|
|||
def lookup_access_level!
|
||||
@subject.max_member_access_for_user(@user)
|
||||
end
|
||||
|
||||
# TODO: Extract this into a helper shared with ProjectPolicy, once we implement group-level features.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/208412
|
||||
def feature_available?(feature)
|
||||
return false unless feature == :wiki
|
||||
|
||||
case @subject.wiki_access_level
|
||||
when ProjectFeature::DISABLED
|
||||
false
|
||||
when ProjectFeature::PRIVATE
|
||||
admin? || access_level >= ProjectFeature.required_minimum_access_level(feature)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
GroupPolicy.prepend_if_ee('EE::GroupPolicy')
|
||||
|
|
|
@ -5,7 +5,7 @@ class IssuePolicy < IssuablePolicy
|
|||
# Make sure to sync this class checks with issue.rb to avoid security problems.
|
||||
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
|
||||
|
||||
extend ProjectPolicy::ClassMethods
|
||||
include CrudPolicyHelpers
|
||||
|
||||
desc "User can read confidential issues"
|
||||
condition(:can_read_confidential) do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectPolicy < BasePolicy
|
||||
extend ClassMethods
|
||||
include CrudPolicyHelpers
|
||||
|
||||
READONLY_FEATURES_WHEN_ARCHIVED = %i[
|
||||
issue
|
||||
|
|
|
@ -16,6 +16,11 @@ module Prometheus
|
|||
identifier: 'response_metrics_nginx_ingress_http_error_rate',
|
||||
operator: 'gt',
|
||||
threshold: 0.1
|
||||
},
|
||||
{
|
||||
identifier: 'response_metrics_nginx_http_error_percentage',
|
||||
operator: 'gt',
|
||||
threshold: 0.1
|
||||
}
|
||||
].freeze
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add filtered search for elastic search in logs
|
||||
merge_request: 27654
|
||||
author:
|
||||
type: added
|
|
@ -194,8 +194,6 @@ application server, or a Gitaly node.
|
|||
- `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
|
||||
|
||||
```ruby
|
||||
# Make Praefect accept connections on all network interfaces.
|
||||
# Use firewalls to restrict access to this address/port.
|
||||
praefect['listen_addr'] = 'PRAEFECT_HOST:2305'
|
||||
|
||||
# Enable Prometheus metrics access to Praefect. You must use firewalls
|
||||
|
@ -532,7 +530,7 @@ Particular attention should be shown to:
|
|||
`/etc/gitlab/gitlab.rb`
|
||||
|
||||
```ruby
|
||||
gitaly['listen_addr'] = 'tcp://GITLAB_HOST:8075'
|
||||
gitaly['listen_addr'] = 'GITLAB_HOST:8075'
|
||||
```
|
||||
|
||||
1. Configure the `gitlab_shell['secret_token']` so that callbacks from Gitaly
|
||||
|
|
|
@ -40,9 +40,13 @@ needs.
|
|||
| Object storage service | Recommended store for shared data objects | [Cloud Object Storage configuration](../high_availability/object_storage.md) |
|
||||
| NFS | Shared disk storage service. Can be used as an alternative for Gitaly or Object Storage. Required for GitLab Pages | [NFS configuration](../high_availability/nfs.md) |
|
||||
|
||||
## Examples
|
||||
## Reference architectures
|
||||
|
||||
### Single-node Omnibus installation
|
||||
- 1 - 1000 Users: A single-node [Omnibus](https://docs.gitlab.com/omnibus/) setup with frequent backups. Refer to the [Single-node Omnibus installation](#single-node-installation) section below.
|
||||
- 1000 to 50000+ Users: A [Scaled-out Omnibus installation with multiple servers](#multi-node-installation-scaled-out-for-availability), it can be with or without high-availability components applied.
|
||||
- To decide the level of Availability please refer to our [Availability](../availability/index.md) page.
|
||||
|
||||
### Single-node installation
|
||||
|
||||
This solution is appropriate for many teams that have a single server at their disposal. With automatic backup of the GitLab repositories, configuration, and the database, this can be an optimal solution if you don't have strict availability requirements.
|
||||
|
||||
|
@ -55,7 +59,7 @@ References:
|
|||
- [Installation Docs](../../install/README.md)
|
||||
- [Backup/Restore Docs](https://docs.gitlab.com/omnibus/settings/backups.html#backup-and-restore-omnibus-gitlab-configuration)
|
||||
|
||||
### Omnibus installation with multiple application servers
|
||||
### Multi-node installation (scaled out for availability)
|
||||
|
||||
This solution is appropriate for teams that are starting to scale out when
|
||||
scaling up is no longer meeting their needs. In this configuration, additional application nodes will handle frontend traffic, with a load balancer in front to distribute traffic across those nodes. Meanwhile, each application node connects to a shared file server and PostgreSQL and Redis services on the back end.
|
||||
|
@ -72,14 +76,6 @@ References:
|
|||
- [Configure packaged PostgreSQL server to listen on TCP/IP](https://docs.gitlab.com/omnibus/settings/database.html#configure-packaged-postgresql-server-to-listen-on-tcpip)
|
||||
- [Setting up a Redis-only server](https://docs.gitlab.com/omnibus/settings/redis.html#setting-up-a-redis-only-server)
|
||||
|
||||
## Recommended setups based on number of users
|
||||
|
||||
- 1 - 1000 Users: A single-node [Omnibus](https://docs.gitlab.com/omnibus/) setup with frequent backups. Refer to the [requirements page](../../install/requirements.md) for further details of the specs you will require.
|
||||
- 1000 - 10000 Users: A scaled environment based on one of our [Reference Architectures](#reference-architectures), without the HA components applied. This can be a reasonable step towards a fully HA environment.
|
||||
- 2000 - 50000+ Users: A scaled HA environment based on one of our [Reference Architectures](#reference-architectures) below.
|
||||
|
||||
## Reference architectures
|
||||
|
||||
In this section we'll detail the Reference Architectures that can support large numbers
|
||||
of users. These were built, tested and verified by our Quality and Support teams.
|
||||
|
||||
|
@ -99,7 +95,7 @@ how much automation you use, mirroring, and repo/change size. Additionally the
|
|||
shown memory values are given directly by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
|
||||
On different cloud vendors a best effort like for like can be used.
|
||||
|
||||
### 2,000 user configuration
|
||||
#### 2,000 user configuration
|
||||
|
||||
- **Supported users (approximate):** 2,000
|
||||
- **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
|
||||
|
@ -120,7 +116,7 @@ On different cloud vendors a best effort like for like can be used.
|
|||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||
|
||||
### 5,000 user configuration
|
||||
#### 5,000 user configuration
|
||||
|
||||
- **Supported users (approximate):** 5,000
|
||||
- **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
|
||||
|
@ -141,7 +137,7 @@ On different cloud vendors a best effort like for like can be used.
|
|||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||
|
||||
### 10,000 user configuration
|
||||
#### 10,000 user configuration
|
||||
|
||||
- **Supported users (approximate):** 10,000
|
||||
- **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
|
||||
|
@ -165,7 +161,7 @@ On different cloud vendors a best effort like for like can be used.
|
|||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||
| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||
|
||||
### 25,000 user configuration
|
||||
#### 25,000 user configuration
|
||||
|
||||
- **Supported users (approximate):** 25,000
|
||||
- **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
|
||||
|
@ -189,7 +185,7 @@ On different cloud vendors a best effort like for like can be used.
|
|||
| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
|
||||
| Internal load balancing node[^6] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
|
||||
|
||||
### 50,000 user configuration
|
||||
#### 50,000 user configuration
|
||||
|
||||
- **Supported users (approximate):** 50,000
|
||||
- **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
|
||||
|
|
BIN
doc/user/project/clusters/img/kubernetes_pod_logs_v12_10.png
Normal file
BIN
doc/user/project/clusters/img/kubernetes_pod_logs_v12_10.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
Before Width: | Height: | Size: 115 KiB |
BIN
doc/user/project/clusters/img/sidebar_menu_pod_logs_v12_10.png
Normal file
BIN
doc/user/project/clusters/img/sidebar_menu_pod_logs_v12_10.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -14,7 +14,7 @@ Everything you need to build, test, deploy, and run your app at scale.
|
|||
|
||||
[Kubernetes](https://kubernetes.io) logs can be viewed directly within GitLab.
|
||||
|
||||
![Pod logs](img/kubernetes_pod_logs_v12_9.png)
|
||||
![Pod logs](img/kubernetes_pod_logs_v12_10.png)
|
||||
|
||||
## Requirements
|
||||
|
||||
|
@ -32,7 +32,7 @@ You can access them in two ways.
|
|||
|
||||
Go to **{cloud-gear}** **Operations > Logs** on the sidebar menu.
|
||||
|
||||
![Sidebar menu](img/sidebar_menu_pod_logs_v12_5.png)
|
||||
![Sidebar menu](img/sidebar_menu_pod_logs_v12_10.png)
|
||||
|
||||
### From Deploy Boards
|
||||
|
||||
|
|
|
@ -7869,9 +7869,6 @@ msgstr ""
|
|||
msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|All pods"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|An error occurred while canceling the auto stop, please try again"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7938,9 +7935,6 @@ msgstr ""
|
|||
msgid "Environments|Environments are places where code gets deployed, such as staging or production."
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Filter by pod"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Install Elastic Stack on your cluster to enable advanced querying capabilities such as full text search."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7980,6 +7974,9 @@ msgstr ""
|
|||
msgid "Environments|Open live environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Pod name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Re-deploy"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8007,9 +8004,6 @@ msgstr ""
|
|||
msgid "Environments|Rollback environment %{name}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Select environment"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19365,12 +19359,6 @@ msgstr ""
|
|||
msgid "Static Application Security Testing (SAST)"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|A merge request was created:"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|A new branch was created:"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Return to site"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19383,10 +19371,16 @@ msgstr ""
|
|||
msgid "StaticSiteEditor|View merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted."
|
||||
msgid "StaticSiteEditor|You added a commit:"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Your changes were committed to it:"
|
||||
msgid "StaticSiteEditor|You created a merge request:"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|You created a new branch:"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Statistics"
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
mockPods,
|
||||
mockLogsResult,
|
||||
mockTrace,
|
||||
mockPodName,
|
||||
mockEnvironmentsEndpoint,
|
||||
mockDocumentationPath,
|
||||
} from '../mock_data';
|
||||
|
@ -302,11 +301,11 @@ describe('EnvironmentLogs', () => {
|
|||
});
|
||||
|
||||
it('refresh button, trace is refreshed', () => {
|
||||
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
|
||||
expect(dispatch).not.toHaveBeenCalledWith(`${module}/fetchLogs`, undefined);
|
||||
|
||||
findLogControlButtons().vm.$emit('refresh');
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPodName);
|
||||
expect(dispatch).toHaveBeenCalledWith(`${module}/fetchLogs`, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { GlIcon, GlDropdownItem } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { defaultTimeRange } from '~/vue_shared/constants';
|
||||
import { GlFilteredSearch } from '@gitlab/ui';
|
||||
import { convertToFixedRange } from '~/lib/utils/datetime_range';
|
||||
import { createStore } from '~/logs/stores';
|
||||
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
|
||||
import { mockPods, mockSearch } from '../mock_data';
|
||||
|
||||
import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
|
||||
|
@ -15,26 +16,19 @@ describe('LogAdvancedFilters', () => {
|
|||
let wrapper;
|
||||
let state;
|
||||
|
||||
const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' });
|
||||
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
|
||||
const findPodsDropdownItems = () =>
|
||||
findPodsDropdown()
|
||||
.findAll(GlDropdownItem)
|
||||
.filter(item => !item.is('[disabled]'));
|
||||
const findPodsDropdownItemsSelected = () =>
|
||||
findPodsDropdownItems()
|
||||
.filter(item => {
|
||||
return !item.find(GlIcon).classes('invisible');
|
||||
})
|
||||
.at(0);
|
||||
const findSearchBox = () => wrapper.find({ ref: 'searchBox' });
|
||||
const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
|
||||
const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
|
||||
const getSearchToken = type =>
|
||||
findFilteredSearch()
|
||||
.props('availableTokens')
|
||||
.filter(token => token.type === type)[0];
|
||||
|
||||
const mockStateLoading = () => {
|
||||
state.timeRange.selected = defaultTimeRange;
|
||||
state.timeRange.current = convertToFixedRange(defaultTimeRange);
|
||||
state.pods.options = [];
|
||||
state.pods.current = null;
|
||||
state.logs.isLoading = true;
|
||||
};
|
||||
|
||||
const mockStateWithData = () => {
|
||||
|
@ -42,6 +36,7 @@ describe('LogAdvancedFilters', () => {
|
|||
state.timeRange.current = convertToFixedRange(defaultTimeRange);
|
||||
state.pods.options = mockPods;
|
||||
state.pods.current = null;
|
||||
state.logs.isLoading = false;
|
||||
};
|
||||
|
||||
const initWrapper = (propsData = {}) => {
|
||||
|
@ -76,11 +71,18 @@ describe('LogAdvancedFilters', () => {
|
|||
expect(wrapper.isVueInstance()).toBe(true);
|
||||
expect(wrapper.isEmpty()).toBe(false);
|
||||
|
||||
expect(findPodsDropdown().exists()).toBe(true);
|
||||
expect(findSearchBox().exists()).toBe(true);
|
||||
expect(findFilteredSearch().exists()).toBe(true);
|
||||
expect(findTimeRangePicker().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays search tokens', () => {
|
||||
expect(getSearchToken(TOKEN_TYPE_POD_NAME)).toMatchObject({
|
||||
title: 'Pod name',
|
||||
unique: true,
|
||||
operators: [expect.objectContaining({ value: '=' })],
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled state', () => {
|
||||
beforeEach(() => {
|
||||
mockStateLoading();
|
||||
|
@ -90,9 +92,7 @@ describe('LogAdvancedFilters', () => {
|
|||
});
|
||||
|
||||
it('displays disabled filters', () => {
|
||||
expect(findPodsDropdown().props('text')).toBe('All pods');
|
||||
expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
|
||||
expect(findSearchBox().attributes('disabled')).toBeTruthy();
|
||||
expect(findFilteredSearch().attributes('disabled')).toBeTruthy();
|
||||
expect(findTimeRangePicker().attributes('disabled')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -103,16 +103,17 @@ describe('LogAdvancedFilters', () => {
|
|||
initWrapper();
|
||||
});
|
||||
|
||||
it('displays a enabled filters', () => {
|
||||
expect(findPodsDropdown().props('text')).toBe('All pods');
|
||||
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
|
||||
expect(findSearchBox().attributes('disabled')).toBeFalsy();
|
||||
it('displays a disabled search', () => {
|
||||
expect(findFilteredSearch().attributes('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('displays an enable date filter', () => {
|
||||
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('displays an empty pods dropdown', () => {
|
||||
expect(findPodsNoPodsText().exists()).toBe(true);
|
||||
expect(findPodsDropdownItems()).toHaveLength(0);
|
||||
it('displays no pod options when no pods are available, so suggestions can be displayed', () => {
|
||||
expect(getSearchToken(TOKEN_TYPE_POD_NAME).options).toBe(null);
|
||||
expect(getSearchToken(TOKEN_TYPE_POD_NAME).loading).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -122,20 +123,24 @@ describe('LogAdvancedFilters', () => {
|
|||
initWrapper();
|
||||
});
|
||||
|
||||
it('displays an enabled pods dropdown', () => {
|
||||
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
|
||||
expect(findPodsDropdown().props('text')).toBe('All pods');
|
||||
it('displays a single token for pods', () => {
|
||||
initWrapper();
|
||||
|
||||
const tokens = findFilteredSearch().props('availableTokens');
|
||||
|
||||
expect(tokens).toHaveLength(1);
|
||||
expect(tokens[0].type).toBe(TOKEN_TYPE_POD_NAME);
|
||||
});
|
||||
|
||||
it('displays options in a pods dropdown', () => {
|
||||
const items = findPodsDropdownItems();
|
||||
expect(items).toHaveLength(mockPods.length + 1);
|
||||
it('displays a enabled filters', () => {
|
||||
expect(findFilteredSearch().attributes('disabled')).toBeFalsy();
|
||||
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('displays "all pods" selected in a pods dropdown', () => {
|
||||
const selected = findPodsDropdownItemsSelected();
|
||||
it('displays options in the pods token', () => {
|
||||
const { options } = getSearchToken(TOKEN_TYPE_POD_NAME);
|
||||
|
||||
expect(selected.text()).toBe('All pods');
|
||||
expect(options).toHaveLength(mockPods.length);
|
||||
});
|
||||
|
||||
it('displays options in date time picker', () => {
|
||||
|
@ -146,30 +151,16 @@ describe('LogAdvancedFilters', () => {
|
|||
});
|
||||
|
||||
describe('when the user interacts', () => {
|
||||
it('clicks on a all options, showPodLogs is dispatched with null', () => {
|
||||
const items = findPodsDropdownItems();
|
||||
items.at(0).vm.$emit('click');
|
||||
it('clicks on the search button, showFilteredLogs is dispatched', () => {
|
||||
findFilteredSearch().vm.$emit('submit', null);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, null);
|
||||
expect(dispatch).toHaveBeenCalledWith(`${module}/showFilteredLogs`, null);
|
||||
});
|
||||
|
||||
it('clicks on a pod name, showPodLogs is dispatched with pod name', () => {
|
||||
const items = findPodsDropdownItems();
|
||||
const index = 2; // any pod
|
||||
it('clicks on the search button, showFilteredLogs is dispatched with null', () => {
|
||||
findFilteredSearch().vm.$emit('submit', [mockSearch]);
|
||||
|
||||
items.at(index + 1).vm.$emit('click'); // skip "All pods" option
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
|
||||
});
|
||||
|
||||
it('clicks on search, a serches is done', () => {
|
||||
expect(findSearchBox().attributes('disabled')).toBeFalsy();
|
||||
|
||||
// input a query and click `search`
|
||||
findSearchBox().vm.$emit('input', mockSearch);
|
||||
findSearchBox().vm.$emit('submit');
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch);
|
||||
expect(dispatch).toHaveBeenCalledWith(`${module}/showFilteredLogs`, [mockSearch]);
|
||||
});
|
||||
|
||||
it('selects a new time range', () => {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import TokenWithLoadingState from '~/logs/components/tokens/token_with_loading_state.vue';
|
||||
|
||||
describe('TokenWithLoadingState', () => {
|
||||
let wrapper;
|
||||
|
||||
const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
|
||||
const initWrapper = (props = {}, options) => {
|
||||
wrapper = shallowMount(TokenWithLoadingState, {
|
||||
propsData: props,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {});
|
||||
|
||||
it('passes entire config correctly', () => {
|
||||
const config = {
|
||||
icon: 'pod',
|
||||
type: 'pod',
|
||||
title: 'Pod name',
|
||||
unique: true,
|
||||
};
|
||||
|
||||
initWrapper({ config });
|
||||
|
||||
expect(findFilteredSearchToken().props('config')).toEqual(config);
|
||||
});
|
||||
|
||||
describe('suggestions are replaced', () => {
|
||||
let mockNoOptsText;
|
||||
let config;
|
||||
let stubs;
|
||||
|
||||
beforeEach(() => {
|
||||
mockNoOptsText = 'No suggestions available';
|
||||
config = {
|
||||
loading: false,
|
||||
noOptionsText: mockNoOptsText,
|
||||
};
|
||||
stubs = {
|
||||
GlFilteredSearchToken: {
|
||||
template: `<div><slot name="suggestions"></slot></div>`,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('renders a loading icon', () => {
|
||||
config.loading = true;
|
||||
|
||||
initWrapper({ config }, { stubs });
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
expect(wrapper.text()).toBe('');
|
||||
});
|
||||
|
||||
it('renders an empty results message', () => {
|
||||
initWrapper({ config }, { stubs });
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
expect(wrapper.text()).toBe(mockNoOptsText);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range';
|
|||
import logsPageState from '~/logs/stores/state';
|
||||
import {
|
||||
setInitData,
|
||||
setSearch,
|
||||
showFilteredLogs,
|
||||
showPodLogs,
|
||||
fetchEnvironments,
|
||||
fetchLogs,
|
||||
|
@ -31,6 +31,7 @@ import {
|
|||
mockCursor,
|
||||
mockNextCursor,
|
||||
} from '../mock_data';
|
||||
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/datetime_range');
|
||||
|
@ -93,13 +94,80 @@ describe('Logs Store actions', () => {
|
|||
));
|
||||
});
|
||||
|
||||
describe('setSearch', () => {
|
||||
it('should commit search mutation', () =>
|
||||
describe('showFilteredLogs', () => {
|
||||
it('empty search should filter with defaults', () =>
|
||||
testAction(
|
||||
setSearch,
|
||||
mockSearch,
|
||||
showFilteredLogs,
|
||||
undefined,
|
||||
state,
|
||||
[{ type: types.SET_SEARCH, payload: mockSearch }],
|
||||
[
|
||||
{ type: types.SET_CURRENT_POD_NAME, payload: null },
|
||||
{ type: types.SET_SEARCH, payload: '' },
|
||||
],
|
||||
[{ type: 'fetchLogs' }],
|
||||
));
|
||||
|
||||
it('text search should filter with a search term', () =>
|
||||
testAction(
|
||||
showFilteredLogs,
|
||||
[mockSearch],
|
||||
state,
|
||||
[
|
||||
{ type: types.SET_CURRENT_POD_NAME, payload: null },
|
||||
{ type: types.SET_SEARCH, payload: mockSearch },
|
||||
],
|
||||
[{ type: 'fetchLogs' }],
|
||||
));
|
||||
|
||||
it('pod search should filter with a search term', () =>
|
||||
testAction(
|
||||
showFilteredLogs,
|
||||
[{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } }],
|
||||
state,
|
||||
[
|
||||
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
|
||||
{ type: types.SET_SEARCH, payload: '' },
|
||||
],
|
||||
[{ type: 'fetchLogs' }],
|
||||
));
|
||||
|
||||
it('pod search should filter with a pod selection and a search term', () =>
|
||||
testAction(
|
||||
showFilteredLogs,
|
||||
[{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } }, mockSearch],
|
||||
state,
|
||||
[
|
||||
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
|
||||
{ type: types.SET_SEARCH, payload: mockSearch },
|
||||
],
|
||||
[{ type: 'fetchLogs' }],
|
||||
));
|
||||
|
||||
it('pod search should filter with a pod selection and two search terms', () =>
|
||||
testAction(
|
||||
showFilteredLogs,
|
||||
['term1', 'term2'],
|
||||
state,
|
||||
[
|
||||
{ type: types.SET_CURRENT_POD_NAME, payload: null },
|
||||
{ type: types.SET_SEARCH, payload: `term1 term2` },
|
||||
],
|
||||
[{ type: 'fetchLogs' }],
|
||||
));
|
||||
|
||||
it('pod search should filter with a pod selection and a search terms before and after', () =>
|
||||
testAction(
|
||||
showFilteredLogs,
|
||||
[
|
||||
'term1',
|
||||
{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } },
|
||||
'term2',
|
||||
],
|
||||
state,
|
||||
[
|
||||
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
|
||||
{ type: types.SET_SEARCH, payload: `term1 term2` },
|
||||
],
|
||||
[{ type: 'fetchLogs' }],
|
||||
));
|
||||
});
|
||||
|
|
|
@ -655,4 +655,26 @@ describe GroupPolicy do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'model with wiki policies' do
|
||||
let(:container) { create(:group) }
|
||||
|
||||
def set_access_level(access_level)
|
||||
allow(container).to receive(:wiki_access_level).and_return(access_level)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(group_wiki: true)
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(group_wiki: false)
|
||||
end
|
||||
|
||||
it 'does not include the wiki permissions' do
|
||||
expect_disallowed(*permissions)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -121,147 +121,11 @@ describe ProjectPolicy do
|
|||
expect(Ability).not_to be_allowed(user, :read_issue, project)
|
||||
end
|
||||
|
||||
context 'wiki feature' do
|
||||
let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) }
|
||||
it_behaves_like 'model with wiki policies' do
|
||||
let(:container) { project }
|
||||
|
||||
subject { described_class.new(owner, project) }
|
||||
|
||||
context 'when the feature is disabled' do
|
||||
before do
|
||||
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
|
||||
end
|
||||
|
||||
it 'does not include the wiki permissions' do
|
||||
expect_disallowed(*permissions)
|
||||
end
|
||||
|
||||
context 'when there is an external wiki' do
|
||||
it 'does not include the wiki permissions' do
|
||||
allow(project).to receive(:has_external_wiki?).and_return(true)
|
||||
|
||||
expect_disallowed(*permissions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'read_wiki' do
|
||||
subject { described_class.new(user, project) }
|
||||
|
||||
member_roles = %i[guest developer]
|
||||
stranger_roles = %i[anonymous non_member]
|
||||
|
||||
user_roles = stranger_roles + member_roles
|
||||
|
||||
# When a user is anonymous, their `current_user == nil`
|
||||
let(:user) { create(:user) unless user_role == :anonymous }
|
||||
|
||||
before do
|
||||
project.visibility = project_visibility
|
||||
project.project_feature.update_attribute(:wiki_access_level, wiki_access_level)
|
||||
project.add_user(user, user_role) if member_roles.include?(user_role)
|
||||
end
|
||||
|
||||
title = ->(project_visibility, wiki_access_level, user_role) do
|
||||
[
|
||||
"project is #{Gitlab::VisibilityLevel.level_name project_visibility}",
|
||||
"wiki is #{ProjectFeature.str_from_access_level wiki_access_level}",
|
||||
"user is #{user_role}"
|
||||
].join(', ')
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki is always false' do
|
||||
where(case_names: title,
|
||||
project_visibility: Gitlab::VisibilityLevel.options.values,
|
||||
wiki_access_level: [ProjectFeature::DISABLED],
|
||||
user_role: user_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki is always true' do
|
||||
where(case_names: title,
|
||||
project_visibility: [Gitlab::VisibilityLevel::PUBLIC],
|
||||
wiki_access_level: [ProjectFeature::ENABLED],
|
||||
user_role: user_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki requires project membership' do
|
||||
context 'the wiki is private, and the user is a member' do
|
||||
where(case_names: title,
|
||||
project_visibility: [Gitlab::VisibilityLevel::PUBLIC,
|
||||
Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::PRIVATE],
|
||||
user_role: member_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the wiki is private, and the user is not member' do
|
||||
where(case_names: title,
|
||||
project_visibility: [Gitlab::VisibilityLevel::PUBLIC,
|
||||
Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::PRIVATE],
|
||||
user_role: stranger_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the wiki is enabled, and the user is a member' do
|
||||
where(case_names: title,
|
||||
project_visibility: [Gitlab::VisibilityLevel::PRIVATE],
|
||||
wiki_access_level: [ProjectFeature::ENABLED],
|
||||
user_role: member_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the wiki is enabled, and the user is not a member' do
|
||||
where(case_names: title,
|
||||
project_visibility: [Gitlab::VisibilityLevel::PRIVATE],
|
||||
wiki_access_level: [ProjectFeature::ENABLED],
|
||||
user_role: stranger_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki prohibits anonymous access' do
|
||||
context 'the user is not anonymous' do
|
||||
where(case_names: title,
|
||||
project_visibility: [Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
|
||||
user_role: user_roles.reject { |u| u == :anonymous })
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the user is not anonymous' do
|
||||
where(case_names: title,
|
||||
project_visibility: [Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
|
||||
user_role: %i[anonymous])
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
end
|
||||
def set_access_level(access_level)
|
||||
project.project_feature.update_attribute(:wiki_access_level, access_level)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -14,16 +14,17 @@ RSpec.shared_context 'GroupPolicy context' do
|
|||
%i[
|
||||
read_label read_group upload_file read_namespace read_group_activity
|
||||
read_group_issues read_group_boards read_group_labels read_group_milestones
|
||||
read_group_merge_requests
|
||||
read_group_merge_requests read_wiki
|
||||
]
|
||||
end
|
||||
let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] }
|
||||
let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] }
|
||||
let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] }
|
||||
let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation download_wiki_code] }
|
||||
let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_wiki] }
|
||||
let(:maintainer_permissions) do
|
||||
%i[
|
||||
create_projects
|
||||
read_cluster create_cluster update_cluster admin_cluster add_cluster
|
||||
admin_wiki
|
||||
]
|
||||
end
|
||||
let(:owner_permissions) do
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'model with wiki policies' do
|
||||
let(:container) { raise NotImplementedError }
|
||||
let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) }
|
||||
|
||||
# TODO: Remove this helper once we implement group features
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/208412
|
||||
def set_access_level(access_level)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
subject { described_class.new(owner, container) }
|
||||
|
||||
context 'when the feature is disabled' do
|
||||
before do
|
||||
set_access_level(ProjectFeature::DISABLED)
|
||||
end
|
||||
|
||||
it 'does not include the wiki permissions' do
|
||||
expect_disallowed(*permissions)
|
||||
end
|
||||
|
||||
context 'when there is an external wiki' do
|
||||
it 'does not include the wiki permissions' do
|
||||
allow(container).to receive(:has_external_wiki?).and_return(true)
|
||||
|
||||
expect_disallowed(*permissions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'read_wiki' do
|
||||
subject { described_class.new(user, container) }
|
||||
|
||||
member_roles = %i[guest developer]
|
||||
stranger_roles = %i[anonymous non_member]
|
||||
|
||||
user_roles = stranger_roles + member_roles
|
||||
|
||||
# When a user is anonymous, their `current_user == nil`
|
||||
let(:user) { create(:user) unless user_role == :anonymous }
|
||||
|
||||
before do
|
||||
container.visibility = container_visibility
|
||||
set_access_level(wiki_access_level)
|
||||
container.add_user(user, user_role) if member_roles.include?(user_role)
|
||||
end
|
||||
|
||||
title = ->(container_visibility, wiki_access_level, user_role) do
|
||||
[
|
||||
"container is #{Gitlab::VisibilityLevel.level_name container_visibility}",
|
||||
"wiki is #{ProjectFeature.str_from_access_level wiki_access_level}",
|
||||
"user is #{user_role}"
|
||||
].join(', ')
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki is always false' do
|
||||
where(case_names: title,
|
||||
container_visibility: Gitlab::VisibilityLevel.options.values,
|
||||
wiki_access_level: [ProjectFeature::DISABLED],
|
||||
user_role: user_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki is always true' do
|
||||
where(case_names: title,
|
||||
container_visibility: [Gitlab::VisibilityLevel::PUBLIC],
|
||||
wiki_access_level: [ProjectFeature::ENABLED],
|
||||
user_role: user_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki requires membership' do
|
||||
context 'the wiki is private, and the user is a member' do
|
||||
where(case_names: title,
|
||||
container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
|
||||
Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::PRIVATE],
|
||||
user_role: member_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the wiki is private, and the user is not member' do
|
||||
where(case_names: title,
|
||||
container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
|
||||
Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::PRIVATE],
|
||||
user_role: stranger_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the wiki is enabled, and the user is a member' do
|
||||
where(case_names: title,
|
||||
container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
|
||||
wiki_access_level: [ProjectFeature::ENABLED],
|
||||
user_role: member_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the wiki is enabled, and the user is not a member' do
|
||||
where(case_names: title,
|
||||
container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
|
||||
wiki_access_level: [ProjectFeature::ENABLED],
|
||||
user_role: stranger_roles)
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Situations where :read_wiki prohibits anonymous access' do
|
||||
context 'the user is not anonymous' do
|
||||
where(case_names: title,
|
||||
container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
|
||||
user_role: user_roles.reject { |u| u == :anonymous })
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_allowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'the user is anonymous' do
|
||||
where(case_names: title,
|
||||
container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
|
||||
wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
|
||||
user_role: %i[anonymous])
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_disallowed(:read_wiki) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue