Commit b77fb046 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 75ee59f7
...@@ -89,10 +89,9 @@ export default { ...@@ -89,10 +89,9 @@ export default {
methods: { methods: {
...mapActions('environmentLogs', [ ...mapActions('environmentLogs', [
'setInitData', 'setInitData',
'setSearch',
'showPodLogs',
'showEnvironment', 'showEnvironment',
'fetchEnvironments', 'fetchEnvironments',
'fetchLogs',
'fetchMoreLogsPrepend', 'fetchMoreLogsPrepend',
'dismissRequestEnvironmentsError', 'dismissRequestEnvironmentsError',
'dismissInvalidTimeRangeWarning', 'dismissInvalidTimeRangeWarning',
...@@ -191,13 +190,13 @@ export default { ...@@ -191,13 +190,13 @@ export default {
<log-advanced-filters <log-advanced-filters
v-if="showAdvancedFilters" v-if="showAdvancedFilters"
ref="log-advanced-filters" ref="log-advanced-filters"
class="d-md-flex flex-grow-1" class="d-md-flex flex-grow-1 min-width-0"
:disabled="environments.isLoading" :disabled="environments.isLoading"
/> />
<log-simple-filters <log-simple-filters
v-else v-else
ref="log-simple-filters" ref="log-simple-filters"
class="d-md-flex flex-grow-1" class="d-md-flex flex-grow-1 min-width-0"
:disabled="environments.isLoading" :disabled="environments.isLoading"
/> />
...@@ -205,7 +204,7 @@ export default { ...@@ -205,7 +204,7 @@ export default {
ref="scrollButtons" ref="scrollButtons"
class="flex-grow-0 pr-2 mb-2 controllers" class="flex-grow-0 pr-2 mb-2 controllers"
:scroll-down-button-disabled="scrollDownButtonDisabled" :scroll-down-button-disabled="scrollDownButtonDisabled"
@refresh="showPodLogs(pods.current)" @refresh="fetchLogs()"
@scrollDown="scrollDown" @scrollDown="scrollDown"
/> />
</div> </div>
......
<script> <script>
import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { import { GlFilteredSearch } from '@gitlab/ui';
GlIcon, import { __, s__ } from '~/locale';
GlDropdown, import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
} from '@gitlab/ui';
import { timeRanges } from '~/vue_shared/constants'; import { timeRanges } from '~/vue_shared/constants';
import { TOKEN_TYPE_POD_NAME } from '../constants';
import TokenWithLoadingState from './tokens/token_with_loading_state.vue';
export default { export default {
components: { components: {
GlIcon, GlFilteredSearch,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
DateTimePicker, DateTimePicker,
}, },
props: { props: {
...@@ -32,11 +22,10 @@ export default { ...@@ -32,11 +22,10 @@ export default {
data() { data() {
return { return {
timeRanges, timeRanges,
searchQuery: '',
}; };
}, },
computed: { computed: {
...mapState('environmentLogs', ['timeRange', 'pods']), ...mapState('environmentLogs', ['timeRange', 'pods', 'logs']),
timeRangeModel: { timeRangeModel: {
get() { get() {
...@@ -46,75 +35,56 @@ export default { ...@@ -46,75 +35,56 @@ export default {
this.setTimeRange(val); 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() { tokens() {
return this.pods.current || s__('Environments|All pods'); 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: { methods: {
...mapActions('environmentLogs', ['setSearch', 'showPodLogs', 'setTimeRange']), ...mapActions('environmentLogs', ['showFilteredLogs', 'setTimeRange']),
isCurrentPod(podName) {
return podName === this.pods.current; filteredSearchSubmit(filters) {
this.showFilteredLogs(filters);
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-dropdown <div class="mb-2 pr-2 flex-grow-1 min-width-0">
ref="podsDropdown" <gl-filtered-search
:text="podDropdownText" :placeholder="__('Search')"
:disabled="disabled" :clear-button-title="__('Clear')"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown" :close-button-title="__('Close')"
> class="gl-h-32"
<gl-dropdown-header class="text-center"> :disabled="disabled || logs.isLoading"
{{ s__('Environments|Filter by pod') }} :available-tokens="tokens"
</gl-dropdown-header> @submit="filteredSearchSubmit"
/>
<gl-dropdown-item v-if="!pods.options.length" disabled> </div>
<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="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 <date-time-picker
ref="dateTimePicker" ref="dateTimePicker"
......
<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>
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'; ...@@ -2,6 +2,7 @@ import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { TOKEN_TYPE_POD_NAME } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -49,19 +50,42 @@ const requestLogsUntilData = ({ commit, state }) => { ...@@ -49,19 +50,42 @@ const requestLogsUntilData = ({ commit, state }) => {
return requestUntilData(logs_api_path, params); 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 }) => { export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => {
commit(types.SET_TIME_RANGE, timeRange); commit(types.SET_TIME_RANGE, timeRange);
commit(types.SET_PROJECT_ENVIRONMENT, environmentName); commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
commit(types.SET_CURRENT_POD_NAME, podName); 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_CURRENT_POD_NAME, podName);
commit(types.SET_SEARCH, search);
dispatch('fetchLogs'); dispatch('fetchLogs');
}; };
export const setSearch = ({ dispatch, commit }, searchQuery) => { export const showPodLogs = ({ dispatch, commit }, podName) => {
commit(types.SET_SEARCH, searchQuery); commit(types.SET_CURRENT_POD_NAME, podName);
dispatch('fetchLogs'); dispatch('fetchLogs');
}; };
......
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { dateFormatMask } from './constants';
const dateFormatMask = 'UTC:mmm dd HH:MM:ss.l"Z"';
/** /**
* Returns a time range (`start`, `end`) where `start` is the * Returns a time range (`start`, `end`) where `start` is the
......
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
<template> <template>
<div> <div>
<div> <div class="border-bottom pb-4">
<h3>{{ s__('StaticSiteEditor|Success!') }}</h3> <h3>{{ s__('StaticSiteEditor|Success!') }}</h3>
<p> <p>
{{ {{
...@@ -45,35 +45,37 @@ export default { ...@@ -45,35 +45,37 @@ export default {
) )
}} }}
</p> </p>
<div> <div class="d-flex justify-content-end">
<gl-new-button ref="returnToSiteButton" :href="returnUrl">{{ <gl-new-button ref="returnToSiteButton" :href="returnUrl">{{
s__('StaticSiteEditor|Return to site') s__('StaticSiteEditor|Return to site')
}}</gl-new-button> }}</gl-new-button>
<gl-new-button ref="mergeRequestButton" :href="mergeRequest.url" variant="info">{{ <gl-new-button
s__('StaticSiteEditor|View merge request') ref="mergeRequestButton"
}}</gl-new-button> class="ml-2"
:href="mergeRequest.url"
variant="success"
>{{ s__('StaticSiteEditor|View merge request') }}</gl-new-button
>
</div> </div>
</div> </div>
<hr /> <div class="pt-2">
<div>
<h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4> <h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4>
<ul> <ul>
<li> <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> <gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link>
</li> </li>
<li> <li>
{{ s__('StaticSiteEditor|Your changes were committed to it:') }} {{ s__('StaticSiteEditor|You created a merge request:') }}
<gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
</li>
<li>
{{ s__('StaticSiteEditor|A merge request was created:') }}
<gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{ <gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{
mergeRequest.label mergeRequest.label
}}</gl-link> }}</gl-link>
</li> </li>
<li>
{{ s__('StaticSiteEditor|You added a commit:') }}
<gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
</li>
</ul> </ul>
</div> </div>
</div> </div>
......
...@@ -96,8 +96,8 @@ ...@@ -96,8 +96,8 @@
} }
.name { .name {
background-color: $filter-name-resting-color; background-color: $white-normal;
color: $filter-name-text-color; color: $gl-text-color-secondary;
border-radius: 2px 0 0 2px; border-radius: 2px 0 0 2px;
margin-right: 1px; margin-right: 1px;
text-transform: capitalize; text-transform: capitalize;
...@@ -105,7 +105,7 @@ ...@@ -105,7 +105,7 @@
.operator { .operator {
background-color: $white-normal; background-color: $white-normal;
color: $filter-value-text-color; color: $gl-text-color;
margin-right: 1px; margin-right: 1px;
} }
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
background-color: $white-normal; background-color: $white-normal;
color: $filter-value-text-color; color: $gl-text-color;
border-radius: 0 2px 2px 0; border-radius: 0 2px 2px 0;
margin-right: 5px; margin-right: 5px;
padding-right: 8px; padding-right: 8px;
...@@ -152,7 +152,7 @@ ...@@ -152,7 +152,7 @@
.filtered-search-token .selected, .filtered-search-token .selected,
.filtered-search-term .selected { .filtered-search-term .selected {
.name { .name {
background-color: $filter-name-selected-color; background-color: $gray-200;
} }
.operator { .operator {
......
...@@ -86,13 +86,13 @@ ...@@ -86,13 +86,13 @@
line-height: 10px; line-height: 10px;
color: $gl-gray-700; color: $gl-gray-700;
vertical-align: middle; vertical-align: middle;
background-color: $kdb-bg; background-color: $gray-50;
border-width: 1px; border-width: 1px;
border-style: solid; 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-image: none;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 -1px 0 $kdb-shadow inset; box-shadow: 0 -1px 0 $gray-400 inset;
} }
h1 { h1 {
......
...@@ -485,7 +485,7 @@ $line-removed-dark: #fac5cd; ...@@ -485,7 +485,7 @@ $line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc; $line-number-old: #f9d7dc;
$line-number-new: #ddfbe6; $line-number-new: #ddfbe6;
$line-number-select: #fbf2da; $line-number-select: #fbf2da;
$line-target-blue: #f6faff; $line-target-blue: $blue-50;
$line-select-yellow: #fcf8e7; $line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd; $line-select-yellow-dark: #f0e2bd;
$dark-diff-match-bg: rgba(255, 255, 255, 0.3); $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
...@@ -698,7 +698,7 @@ $logs-p-color: #333; ...@@ -698,7 +698,7 @@ $logs-p-color: #333;
*/ */
$input-height: 34px; $input-height: 34px;
$input-danger-bg: #f2dede; $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: rgba(0, 0, 0, 0.075);
$gl-field-focus-shadow-error: rgba($red-500, 0.6); $gl-field-focus-shadow-error: rgba($red-500, 0.6);
$input-short-width: 200px; $input-short-width: 200px;
...@@ -774,9 +774,6 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647); ...@@ -774,9 +774,6 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
/* /*
* Typography * Typography
*/ */
$kdb-bg: #fcfcfc;
$kdb-border-bottom: #bbb;
$kdb-shadow: #bbb;
$body-text-shadow: rgba(255, 255, 255, 0.01); $body-text-shadow: rgba(255, 255, 255, 0.01);
/* /*
...@@ -800,20 +797,6 @@ CI variable lists ...@@ -800,20 +797,6 @@ CI variable lists
*/ */
$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); $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 GitLab Plans
*/ */
......
...@@ -54,6 +54,11 @@ ...@@ -54,6 +54,11 @@
.mh-50vh { max-height: 50vh; } .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; } .font-size-inherit { font-size: inherit; }
.gl-w-8 { width: px-to-rem($grid-size); } .gl-w-8 { width: px-to-rem($grid-size); }
.gl-w-16 { width: px-to-rem($grid-size * 2); } .gl-w-16 { width: px-to-rem($grid-size * 2); }
......
...@@ -475,6 +475,16 @@ class Group < Namespace ...@@ -475,6 +475,16 @@ class Group < Namespace
false false
end 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 private
def update_two_factor_requirement def update_two_factor_requirement
......
# frozen_string_literal: true # frozen_string_literal: true
class ProjectPolicy module CrudPolicyHelpers
module ClassMethods extend ActiveSupport::Concern
class_methods do
def create_read_update_admin_destroy(name) def create_read_update_admin_destroy(name)
[ [
:"read_#{name}", :"read_#{name}",
......
# frozen_string_literal: true # frozen_string_literal: true
class GroupPolicy < BasePolicy class GroupPolicy < BasePolicy
include CrudPolicyHelpers
include FindGroupProjects include FindGroupProjects
desc "Group is public" desc "Group is public"
...@@ -42,15 +43,23 @@ class GroupPolicy < BasePolicy ...@@ -42,15 +43,23 @@ class GroupPolicy < BasePolicy
@subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS @subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS
end end
desc "Group has wiki disabled"
condition(:wiki_disabled, score: 32) { !feature_available?(:wiki) }
rule { public_group }.policy do rule { public_group }.policy do
enable :read_group enable :read_group
enable :read_package enable :read_package
enable :read_wiki
end 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 rule { guest }.policy do
enable :read_group enable :read_group
enable :read_wiki
enable :upload_file enable :upload_file
end end
...@@ -78,10 +87,12 @@ class GroupPolicy < BasePolicy ...@@ -78,10 +87,12 @@ class GroupPolicy < BasePolicy
enable :create_metrics_dashboard_annotation enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation
enable :create_wiki
end end
rule { reporter }.policy do rule { reporter }.policy do
enable :read_container_image enable :read_container_image
enable :download_wiki_code
enable :admin_label enable :admin_label
enable :admin_list enable :admin_list
enable :admin_issue enable :admin_issue
...@@ -100,6 +111,7 @@ class GroupPolicy < BasePolicy ...@@ -100,6 +111,7 @@ class GroupPolicy < BasePolicy
enable :destroy_deploy_token enable :destroy_deploy_token
enable :read_deploy_token enable :read_deploy_token
enable :create_deploy_token enable :create_deploy_token
enable :admin_wiki
end end
rule { owner }.policy do rule { owner }.policy do
...@@ -145,6 +157,11 @@ class GroupPolicy < BasePolicy ...@@ -145,6 +157,11 @@ class GroupPolicy < BasePolicy
rule { maintainer & can?(:create_projects) }.enable :transfer_projects 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 def access_level
return GroupMember::NO_ACCESS if @user.nil? return GroupMember::NO_ACCESS if @user.nil?
...@@ -154,6 +171,21 @@ class GroupPolicy < BasePolicy ...@@ -154,6 +171,21 @@ class GroupPolicy < BasePolicy
def lookup_access_level! def lookup_access_level!
@subject.max_member_access_for_user(@user) @subject.max_member_access_for_user(@user)
end 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 end
GroupPolicy.prepend_if_ee('EE::GroupPolicy') GroupPolicy.prepend_if_ee('EE::GroupPolicy')
...@@ -5,7 +5,7 @@ class IssuePolicy < IssuablePolicy ...@@ -5,7 +5,7 @@ class IssuePolicy < IssuablePolicy
# Make sure to sync this class checks with issue.rb to avoid security problems. # Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
extend ProjectPolicy::ClassMethods include CrudPolicyHelpers
desc "User can read confidential issues" desc "User can read confidential issues"
condition(:can_read_confidential) do condition(:can_read_confidential) do
......
# frozen_string_literal: true # frozen_string_literal: true
class ProjectPolicy < BasePolicy class ProjectPolicy < BasePolicy
extend ClassMethods include CrudPolicyHelpers
READONLY_FEATURES_WHEN_ARCHIVED = %i[ READONLY_FEATURES_WHEN_ARCHIVED = %i[
issue issue
......
...@@ -16,6 +16,11 @@ module Prometheus ...@@ -16,6 +16,11 @@ module Prometheus
identifier: 'response_metrics_nginx_ingress_http_error_rate', identifier: 'response_metrics_nginx_ingress_http_error_rate',
operator: 'gt', operator: 'gt',
threshold: 0.1 threshold: 0.1
},
{
identifier: 'response_metrics_nginx_http_error_percentage',
operator: 'gt',
threshold: 0.1
} }
].freeze ].freeze
......
---
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. ...@@ -194,8 +194,6 @@ application server, or a Gitaly node.
- `PRAEFECT_HOST` with the IP address or hostname of the Praefect node - `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
```ruby ```ruby
# Make Praefect accept connections on all network interfaces.
# Use firewalls to restrict access to this address/port.
praefect['listen_addr'] = 'PRAEFECT_HOST:2305' praefect['listen_addr'] = 'PRAEFECT_HOST:2305'
# Enable Prometheus metrics access to Praefect. You must use firewalls # Enable Prometheus metrics access to Praefect. You must use firewalls
...@@ -532,7 +530,7 @@ Particular attention should be shown to: ...@@ -532,7 +530,7 @@ Particular attention should be shown to:
`/etc/gitlab/gitlab.rb` `/etc/gitlab/gitlab.rb`
```ruby ```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 1. Configure the `gitlab_shell['secret_token']` so that callbacks from Gitaly
......
...@@ -40,9 +40,13 @@ needs. ...@@ -40,9 +40,13 @@ needs.
| Object storage service | Recommended store for shared data objects | [Cloud Object Storage configuration](../high_availability/object_storage.md) | | 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) | | 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
- 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 Omnibus installation ### 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. 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: ...@@ -55,7 +59,7 @@ References:
- [Installation Docs](../../install/README.md) - [Installation Docs](../../install/README.md)
- [Backup/Restore Docs](https://docs.gitlab.com/omnibus/settings/backups.html#backup-and-restore-omnibus-gitlab-configuration) - [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 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. 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: ...@@ -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) - [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) - [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 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. 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 ...@@ -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). 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. 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 - **Supported users (approximate):** 2,000
- **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS - **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. ...@@ -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 | | 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 | | 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 - **Supported users (approximate):** 5,000
- **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS - **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. ...@@ -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 | | 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 | | 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 - **Supported users (approximate):** 10,000
- **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS - **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. ...@@ -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 | | 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 | | 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 - **Supported users (approximate):** 25,000
- **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS - **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. ...@@ -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 | | 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 | | 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 - **Supported users (approximate):** 50,000
- **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS - **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
......
...@@ -14,7 +14,7 @@ Everything you need to build, test, deploy, and run your app at scale. ...@@ -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. [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 ## Requirements
...@@ -32,7 +32,7 @@ You can access them in two ways. ...@@ -32,7 +32,7 @@ You can access them in two ways.
Go to **{cloud-gear}** **Operations > Logs** on the sidebar menu. 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 ### From Deploy Boards
......
...@@ -7869,9 +7869,6 @@ msgstr "" ...@@ -7869,9 +7869,6 @@ msgstr ""
msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}" msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}"
msgstr "" msgstr ""
msgid "Environments|All pods"
msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again" msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr "" msgstr ""
...@@ -7938,9 +7935,6 @@ msgstr "" ...@@ -7938,9 +7935,6 @@ msgstr ""
msgid "Environments|Environments are places where code gets deployed, such as staging or production." msgid "Environments|Environments are places where code gets deployed, such as staging or production."
msgstr "" 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." msgid "Environments|Install Elastic Stack on your cluster to enable advanced querying capabilities such as full text search."
msgstr "" msgstr ""
...@@ -7980,6 +7974,9 @@ msgstr "" ...@@ -7980,6 +7974,9 @@ msgstr ""
msgid "Environments|Open live environment" msgid "Environments|Open live environment"
msgstr "" msgstr ""
msgid "Environments|Pod name"
msgstr ""
msgid "Environments|Re-deploy" msgid "Environments|Re-deploy"
msgstr "" msgstr ""
...@@ -8007,9 +8004,6 @@ msgstr "" ...@@ -8007,9 +8004,6 @@ msgstr ""
msgid "Environments|Rollback environment %{name}?" msgid "Environments|Rollback environment %{name}?"
msgstr "" msgstr ""
msgid "Environments|Search"
msgstr ""
msgid "Environments|Select environment" msgid "Environments|Select environment"
msgstr "" msgstr ""
...@@ -19365,12 +19359,6 @@ msgstr "" ...@@ -19365,12 +19359,6 @@ msgstr ""
msgid "Static Application Security Testing (SAST)" msgid "Static Application Security Testing (SAST)"
msgstr "" msgstr ""
msgid "StaticSiteEditor|A merge request was created:"
msgstr ""
msgid "StaticSiteEditor|A new branch was created:"
msgstr ""
msgid "StaticSiteEditor|Return to site" msgid "StaticSiteEditor|Return to site"
msgstr "" msgstr ""
...@@ -19383,10 +19371,16 @@ msgstr "" ...@@ -19383,10 +19371,16 @@ msgstr ""
msgid "StaticSiteEditor|View merge request" msgid "StaticSiteEditor|View merge request"
msgstr "" 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 "" 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 "" msgstr ""
msgid "Statistics" msgid "Statistics"
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
mockPods, mockPods,
mockLogsResult, mockLogsResult,
mockTrace, mockTrace,
mockPodName,
mockEnvironmentsEndpoint, mockEnvironmentsEndpoint,
mockDocumentationPath, mockDocumentationPath,
} from '../mock_data'; } from '../mock_data';
...@@ -302,11 +301,11 @@ describe('EnvironmentLogs', () => { ...@@ -302,11 +301,11 @@ describe('EnvironmentLogs', () => {
}); });
it('refresh button, trace is refreshed', () => { 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'); findLogControlButtons().vm.$emit('refresh');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPodName); expect(dispatch).toHaveBeenCalledWith(`${module}/fetchLogs`, undefined);
}); });
}); });
}); });
......
import { GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { defaultTimeRange } from '~/vue_shared/constants'; import { defaultTimeRange } from '~/vue_shared/constants';
import { GlFilteredSearch } from '@gitlab/ui';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { createStore } from '~/logs/stores'; import { createStore } from '~/logs/stores';
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
import { mockPods, mockSearch } from '../mock_data'; import { mockPods, mockSearch } from '../mock_data';
import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue'; import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
...@@ -15,26 +16,19 @@ describe('LogAdvancedFilters', () => { ...@@ -15,26 +16,19 @@ describe('LogAdvancedFilters', () => {
let wrapper; let wrapper;
let state; let state;
const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' }); const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
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 findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' }); const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
const getSearchToken = type =>
findFilteredSearch()
.props('availableTokens')
.filter(token => token.type === type)[0];
const mockStateLoading = () => { const mockStateLoading = () => {
state.timeRange.selected = defaultTimeRange; state.timeRange.selected = defaultTimeRange;
state.timeRange.current = convertToFixedRange(defaultTimeRange); state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = []; state.pods.options = [];
state.pods.current = null; state.pods.current = null;
state.logs.isLoading = true;
}; };
const mockStateWithData = () => { const mockStateWithData = () => {
...@@ -42,6 +36,7 @@ describe('LogAdvancedFilters', () => { ...@@ -42,6 +36,7 @@ describe('LogAdvancedFilters', () => {
state.timeRange.current = convertToFixedRange(defaultTimeRange); state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = mockPods; state.pods.options = mockPods;
state.pods.current = null; state.pods.current = null;
state.logs.isLoading = false;
}; };
const initWrapper = (propsData = {}) => { const initWrapper = (propsData = {}) => {
...@@ -76,11 +71,18 @@ describe('LogAdvancedFilters', () => { ...@@ -76,11 +71,18 @@ describe('LogAdvancedFilters', () => {
expect(wrapper.isVueInstance()).toBe(true); expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false); expect(wrapper.isEmpty()).toBe(false);
expect(findPodsDropdown().exists()).toBe(true); expect(findFilteredSearch().exists()).toBe(true);
expect(findSearchBox().exists()).toBe(true);
expect(findTimeRangePicker().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', () => { describe('disabled state', () => {
beforeEach(() => { beforeEach(() => {
mockStateLoading(); mockStateLoading();
...@@ -90,9 +92,7 @@ describe('LogAdvancedFilters', () => { ...@@ -90,9 +92,7 @@ describe('LogAdvancedFilters', () => {
}); });
it('displays disabled filters', () => { it('displays disabled filters', () => {
expect(findPodsDropdown().props('text')).toBe('All pods'); expect(findFilteredSearch().attributes('disabled')).toBeTruthy();
expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
expect(findSearchBox().attributes('disabled')).toBeTruthy();
expect(findTimeRangePicker().attributes('disabled')).toBeTruthy(); expect(findTimeRangePicker().attributes('disabled')).toBeTruthy();
}); });
}); });
...@@ -103,16 +103,17 @@ describe('LogAdvancedFilters', () => { ...@@ -103,16 +103,17 @@ describe('LogAdvancedFilters', () => {
initWrapper(); initWrapper();
}); });
it('displays a enabled filters', () => { it('displays a disabled search', () => {
expect(findPodsDropdown().props('text')).toBe('All pods'); expect(findFilteredSearch().attributes('disabled')).toBeTruthy();
expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); });
expect(findSearchBox().attributes('disabled')).toBeFalsy();
it('displays an enable date filter', () => {
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
}); });
it('displays an empty pods dropdown', () => { it('displays no pod options when no pods are available, so suggestions can be displayed', () => {
expect(findPodsNoPodsText().exists()).toBe(true); expect(getSearchToken(TOKEN_TYPE_POD_NAME).options).toBe(null);
expect(findPodsDropdownItems()).toHaveLength(0); expect(getSearchToken(TOKEN_TYPE_POD_NAME).loading).toBe(true);
}); });
}); });
...@@ -122,20 +123,24 @@ describe('LogAdvancedFilters', () => { ...@@ -122,20 +123,24 @@ describe('LogAdvancedFilters', () => {
initWrapper(); initWrapper();
}); });
it('displays an enabled pods dropdown', () => { it('displays a single token for pods', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); initWrapper();
expect(findPodsDropdown().props('text')).toBe('All pods');
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', () => { it('displays a enabled filters', () => {
const items = findPodsDropdownItems(); expect(findFilteredSearch().attributes('disabled')).toBeFalsy();
expect(items).toHaveLength(mockPods.length + 1); expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
}); });
it('displays "all pods" selected in a pods dropdown', () => { it('displays options in the pods token', () => {
const selected = findPodsDropdownItemsSelected(); 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', () => { it('displays options in date time picker', () => {
...@@ -146,30 +151,16 @@ describe('LogAdvancedFilters', () => { ...@@ -146,30 +151,16 @@ describe('LogAdvancedFilters', () => {
}); });
describe('when the user interacts', () => { describe('when the user interacts', () => {
it('clicks on a all options, showPodLogs is dispatched with null', () => { it('clicks on the search button, showFilteredLogs is dispatched', () => {
const items = findPodsDropdownItems(); findFilteredSearch().vm.$emit('submit', null);
items.at(0).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, null);
});
it('clicks on a pod name, showPodLogs is dispatched with pod name', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
items.at(index + 1).vm.$emit('click'); // skip "All pods" option expect(dispatch).toHaveBeenCalledWith(`${module}/showFilteredLogs`, null);
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
}); });
it('clicks on search, a serches is done', () => { it('clicks on the search button, showFilteredLogs is dispatched with null', () => {
expect(findSearchBox().attributes('disabled')).toBeFalsy(); findFilteredSearch().vm.$emit('submit', [mockSearch]);
// 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', () => { it('selects a new time range', () => {
......
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'; ...@@ -6,7 +6,7 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range';
import logsPageState from '~/logs/stores/state'; import logsPageState from '~/logs/stores/state';
import { import {
setInitData, setInitData,
setSearch, showFilteredLogs,
showPodLogs, showPodLogs,
fetchEnvironments, fetchEnvironments,
fetchLogs, fetchLogs,
...@@ -31,6 +31,7 @@ import { ...@@ -31,6 +31,7 @@ import {
mockCursor, mockCursor,
mockNextCursor, mockNextCursor,
} from '../mock_data'; } from '../mock_data';
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/datetime_range'); jest.mock('~/lib/utils/datetime_range');
...@@ -93,13 +94,80 @@ describe('Logs Store actions', () => { ...@@ -93,13 +94,80 @@ describe('Logs Store actions', () => {
)); ));
}); });
describe('setSearch', () => { describe('showFilteredLogs', () => {
it('should commit search mutation', () => it('empty search should filter with defaults', () =>
testAction( testAction(
setSearch, showFilteredLogs,
mockSearch, undefined,
state, 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' }], [{ type: 'fetchLogs' }],
)); ));
}); });
......
...@@ -655,4 +655,26 @@ describe GroupPolicy do ...@@ -655,4 +655,26 @@ describe GroupPolicy do
end end
end 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 end
...@@ -121,147 +121,11 @@ describe ProjectPolicy do ...@@ -121,147 +121,11 @@ describe ProjectPolicy do
expect(Ability).not_to be_allowed(user, :read_issue, project) expect(Ability).not_to be_allowed(user, :read_issue, project)
end end
context 'wiki feature' do it_behaves_like 'model with wiki policies' do
let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) } let(:container) { project }
subject { described_class.new(owner, project) } def set_access_level(access_level)
project.project_feature.update_attribute(:wiki_access_level, access_level)
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
end end
end end
......
...@@ -14,16 +14,17 @@ RSpec.shared_context 'GroupPolicy context' do ...@@ -14,16 +14,17 @@ RSpec.shared_context 'GroupPolicy context' do
%i[ %i[
read_label read_group upload_file read_namespace read_group_activity 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_issues read_group_boards read_group_labels read_group_milestones
read_group_merge_requests read_group_merge_requests read_wiki
] ]
end end
let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } 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(: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] } let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_wiki] }
let(:maintainer_permissions) do let(:maintainer_permissions) do
%i[ %i[
create_projects create_projects
read_cluster create_cluster update_cluster admin_cluster add_cluster read_cluster create_cluster update_cluster admin_cluster add_cluster
admin_wiki
] ]
end end
let(:owner_permissions) do let(:owner_permissions) do
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment