Commit a80340d4 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 5969c278 ab3055aa
...@@ -44,6 +44,8 @@ export default { ...@@ -44,6 +44,8 @@ export default {
data() { data() {
return { return {
isSubmitEnabled: true, isSubmitEnabled: true,
darkModeOnCreate: null,
darkModeOnSubmit: null,
}; };
}, },
computed: { computed: {
...@@ -58,6 +60,7 @@ export default { ...@@ -58,6 +60,7 @@ export default {
this.formEl.addEventListener('ajax:beforeSend', this.handleLoading); this.formEl.addEventListener('ajax:beforeSend', this.handleLoading);
this.formEl.addEventListener('ajax:success', this.handleSuccess); this.formEl.addEventListener('ajax:success', this.handleSuccess);
this.formEl.addEventListener('ajax:error', this.handleError); this.formEl.addEventListener('ajax:error', this.handleError);
this.darkModeOnCreate = this.darkModeSelected();
}, },
beforeDestroy() { beforeDestroy() {
this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading); this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading);
...@@ -65,16 +68,27 @@ export default { ...@@ -65,16 +68,27 @@ export default {
this.formEl.removeEventListener('ajax:error', this.handleError); this.formEl.removeEventListener('ajax:error', this.handleError);
}, },
methods: { methods: {
darkModeSelected() {
const theme = this.getSelectedTheme();
return theme ? theme.css_class === 'gl-dark' : null;
},
getSelectedTheme() {
const themeId = new FormData(this.formEl).get('user[theme_id]');
return this.applicationThemes[themeId] ?? null;
},
handleLoading() { handleLoading() {
this.isSubmitEnabled = false; this.isSubmitEnabled = false;
this.darkModeOnSubmit = this.darkModeSelected();
}, },
handleSuccess(customEvent) { handleSuccess(customEvent) {
const formData = new FormData(this.formEl); // Reload the page if the theme has changed from light to dark mode or vice versa
updateClasses( // to correctly load all required styles.
this.bodyClasses, const modeChanged = this.darkModeOnCreate ? !this.darkModeOnSubmit : this.darkModeOnSubmit;
this.applicationThemes[formData.get('user[theme_id]')].css_class, if (modeChanged) {
this.selectedLayout, window.location.reload();
); return;
}
updateClasses(this.bodyClasses, this.getSelectedTheme().css_class, this.selectedLayout);
const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } = const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } =
customEvent?.detail?.[0] || {}; customEvent?.detail?.[0] || {};
createFlash({ message, type }); createFlash({ message, type });
......
...@@ -84,11 +84,11 @@ body { ...@@ -84,11 +84,11 @@ body {
&.gl-dark { &.gl-dark {
.logo-text svg { .logo-text svg {
fill: $gl-text-color; fill: var(--gl-text-color);
} }
.navbar-gitlab { .navbar-gitlab {
background-color: $gray-50; background-color: var(--gray-50);
.navbar-sub-nav, .navbar-sub-nav,
.navbar-nav { .navbar-nav {
...@@ -97,8 +97,8 @@ body { ...@@ -97,8 +97,8 @@ body {
> a:focus, > a:focus,
> button:hover, > button:hover,
> button:focus { > button:focus {
color: $gl-text-color; color: var(--gl-text-color);
background-color: $gray-200; background-color: var(--gray-200);
} }
} }
...@@ -106,21 +106,21 @@ body { ...@@ -106,21 +106,21 @@ body {
li.dropdown.show { li.dropdown.show {
> a, > a,
> button { > button {
color: $gl-text-color; color: var(--gl-text-color);
background-color: $gray-200; background-color: var(--gray-200);
} }
} }
} }
.search { .search {
form { form {
background-color: $gray-100; background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px $border-color; box-shadow: inset 0 0 0 1px var(--border-color);
&:active, &:active,
&:hover { &:hover {
background-color: $gray-100; background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px $blue-200; box-shadow: inset 0 0 0 1px var(--blue-200);
} }
} }
} }
......
...@@ -6,5 +6,5 @@ ...@@ -6,5 +6,5 @@
= form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f| = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions .form-actions
= f.submit _('Save changes'), class: 'btn gl-button btn-success' = f.submit _('Save changes'), class: 'btn gl-button btn-confirm'
= link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-cancel' = link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- if @deploy_keys.any? - if @deploy_keys.any?
%h3.page-title.deploy-keys-title %h3.page-title.deploy-keys-title
= _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size } = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
= link_to _('New deploy key'), new_admin_deploy_key_path, class: 'float-right btn gl-button btn-success btn-md gl-button' = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'float-right btn gl-button btn-confirm btn-md gl-button'
.table-holder.deploy-keys-list .table-holder.deploy-keys-list
%table.table %table.table
%thead %thead
......
...@@ -6,5 +6,5 @@ ...@@ -6,5 +6,5 @@
= form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f| = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions .form-actions
= f.submit 'Create', class: 'btn gl-button btn-success' = f.submit 'Create', class: 'btn gl-button btn-confirm'
= link_to 'Cancel', admin_deploy_keys_path, class: 'btn gl-button btn-cancel' = link_to 'Cancel', admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'
---
title: Fix 500 error for long commit messages
merge_request: 55320
author:
type: fixed
---
title: Correctly style Dark Mode application header in profile preferences
merge_request: 54575
author: Simon Stieger @sim0
type: fixed
---
title: Move to btn-confirm from btn-success in admin/deploy_keys directory
merge_request: 55267
author: Yogi (@yo)
type: changed
...@@ -245,3 +245,9 @@ Upload.find_each do |upload| ...@@ -245,3 +245,9 @@ Upload.find_each do |upload|
end end
p "#{uploads_deleted} remote objects were destroyed." p "#{uploads_deleted} remote objects were destroyed."
``` ```
### Delete references to missing LFS objects
If `gitlab-rake gitlab:lfs:check VERBOSE=1` detects LFS objects that exist in the database
but not on disk, [follow the procedure in the LFS documentation](../../topics/git/lfs/index.md#missing-lfs-objects)
to remove the database entries.
...@@ -23,14 +23,12 @@ you never miss a page. ...@@ -23,14 +23,12 @@ you never miss a page.
## Email notifications ## Email notifications
Email notifications are available in projects that have been Email notifications are available in projects for triggered alerts. Project
[configured to create incidents automatically](incidents.md#create-incidents-automatically) members with the **Owner** or **Maintainer** roles have the option to receive
for triggered alerts. Project members with the **Owner** or **Maintainer** roles are a single email notification for new alerts.
sent an email notification automatically. (This is not configurable.) To optionally
send additional email notifications to project members with the **Developer** role:
1. Navigate to **Settings > Operations**. 1. Navigate to **Settings > Operations**.
1. Expand the **Incidents** section. 1. Expand the **Incidents** section.
1. In the **Alert Integration** tab, select the **Send a separate email notification to Developers** 1. In the **Alert Integration** tab, select the checkbox
check box. **Send a single email notification to Owners and Maintainers for new alerts**.
1. Select **Save changes**. 1. Select **Save changes**.
...@@ -269,3 +269,46 @@ You might choose to do this if you are using an appliance like a <!-- vale gitla ...@@ -269,3 +269,46 @@ You might choose to do this if you are using an appliance like a <!-- vale gitla
GitLab can't verify LFS objects. Pushes then fail if you have GitLab LFS support enabled. GitLab can't verify LFS objects. Pushes then fail if you have GitLab LFS support enabled.
To stop push failure, LFS support can be disabled in the [Project settings](../../../user/project/settings/index.md), which also disables GitLab LFS value-adds (Verifying LFS objects, UI integration for LFS). To stop push failure, LFS support can be disabled in the [Project settings](../../../user/project/settings/index.md), which also disables GitLab LFS value-adds (Verifying LFS objects, UI integration for LFS).
### Missing LFS objects
An error about a missing LFS object may occur in either of these situations:
- When migrating LFS objects from disk to object storage, with error messages like:
```plaintext
ERROR -- : Failed to transfer LFS object
006622269c61b41bf14a22bbe0e43be3acf86a4a446afb4250c3794ea47541a7
with error: No such file or directory @ rb_sysopen -
/var/opt/gitlab/gitlab-rails/shared/lfs-objects/00/66/22269c61b41bf14a22bbe0e43be3acf86a4a446afb4250c3794ea47541a7
```
(Line breaks have been added for legibility.)
- When running the
[integrity check for LFS objects](../../../administration/raketasks/check.md#uploaded-files-integrity)
with the `VERBOSE=1` parameter.
The database can have records for LFS objects which are not on disk. The database entry may
[prevent a new copy of the object being pushed](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/49241).
To delete these references:
1. [Start a rails console](../../../administration/operations/rails_console.md).
1. Query the object that's reported as missing in the rails console, to return a file path:
```ruby
lfs_object = LfsObject.find_by(oid: '006622269c61b41bf14a22bbe0e43be3acf86a4a446afb4250c3794ea47541a7')
lfs_object.file.path
```
1. Check on disk if it exists:
```shell
ls -al /var/opt/gitlab/gitlab-rails/shared/lfs-objects/00/66/22269c61b41bf14a22bbe0e43be3acf86a4a446afb4250c3794ea47541a7
```
1. If the file is not present, remove the database record via the rails console:
```ruby
lfs_object.destroy
```
...@@ -104,12 +104,12 @@ module Gitlab ...@@ -104,12 +104,12 @@ module Gitlab
end end
def fetch_last_cached_commits_list def fetch_last_cached_commits_list
cache_key = ['projects', project.id, 'last_commits_list', commit.id, ensured_path, offset, limit] cache_key = ['projects', project.id, 'last_commits', commit.id, ensured_path, offset, limit]
commits = Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do commits = Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
repository repository
.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true) .list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true)
.transform_values!(&:to_hash) .transform_values! { |commit| commit_to_hash(commit) }
end end
commits.transform_values! { |value| Commit.from_hash(value, project) } commits.transform_values! { |value| Commit.from_hash(value, project) }
...@@ -121,6 +121,12 @@ module Gitlab ...@@ -121,6 +121,12 @@ module Gitlab
resolved_commits[commit.id] ||= commit resolved_commits[commit.id] ||= commit
end end
def commit_to_hash(commit)
commit.to_hash.tap do |hash|
hash[:message] = hash[:message].to_s.truncate_bytes(1.kilobyte, omission: '...')
end
end
def commit_path(commit) def commit_path(commit)
Gitlab::Routing.url_helpers.project_commit_path(project, commit) Gitlab::Routing.url_helpers.project_commit_path(project, commit)
end end
......
...@@ -16,11 +16,11 @@ gitlab: ...@@ -16,11 +16,11 @@ gitlab:
gitaly: gitaly:
resources: resources:
requests: requests:
cpu: 1200m cpu: 2400m
memory: 245M memory: 1000M
limits: limits:
cpu: 1800m cpu: 3600m
memory: 367M memory: 1500M
persistence: persistence:
size: 10G size: 10G
gitlab-exporter: gitlab-exporter:
...@@ -38,10 +38,10 @@ gitlab: ...@@ -38,10 +38,10 @@ gitlab:
resources: resources:
requests: requests:
cpu: 500m cpu: 500m
memory: 25M memory: 100M
limits: limits:
cpu: 750m cpu: 750m
memory: 37.5M memory: 150M
maxReplicas: 3 maxReplicas: 3
hpa: hpa:
targetAverageValue: 500m targetAverageValue: 500m
...@@ -52,10 +52,10 @@ gitlab: ...@@ -52,10 +52,10 @@ gitlab:
resources: resources:
requests: requests:
cpu: 855m cpu: 855m
memory: 1285M memory: 1927M
limits: limits:
cpu: 1282m cpu: 1282m
memory: 1927M memory: 2890M
hpa: hpa:
targetAverageValue: 650m targetAverageValue: 650m
task-runner: task-runner:
...@@ -138,10 +138,10 @@ postgresql: ...@@ -138,10 +138,10 @@ postgresql:
resources: resources:
requests: requests:
cpu: 550m cpu: 550m
memory: 250M memory: 1000M
limits: limits:
cpu: 825m cpu: 825m
memory: 375M memory: 1500M
prometheus: prometheus:
install: false install: false
redis: redis:
......
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IntegrationView from '~/profile/preferences/components/integration_view.vue'; import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue'; import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
import { i18n } from '~/profile/preferences/constants'; import { i18n } from '~/profile/preferences/constants';
import { integrationViews, userFields, bodyClasses } from '../mock_data'; import {
integrationViews,
userFields,
bodyClasses,
themes,
lightModeThemeId1,
darkModeThemeId,
lightModeThemeId2,
} from '../mock_data';
const expectedUrl = '/foo'; const expectedUrl = '/foo';
...@@ -14,7 +23,7 @@ describe('ProfilePreferences component', () => { ...@@ -14,7 +23,7 @@ describe('ProfilePreferences component', () => {
integrationViews: [], integrationViews: [],
userFields, userFields,
bodyClasses, bodyClasses,
themes: [{ id: 1, css_class: 'foo' }], themes,
profilePreferencesPath: '/update-profile', profilePreferencesPath: '/update-profile',
formEl: document.createElement('form'), formEl: document.createElement('form'),
}; };
...@@ -49,6 +58,30 @@ describe('ProfilePreferences component', () => { ...@@ -49,6 +58,30 @@ describe('ProfilePreferences component', () => {
return document.querySelector('.flash-container .flash-text'); return document.querySelector('.flash-container .flash-text');
} }
function createThemeInput(themeId = lightModeThemeId1) {
const input = document.createElement('input');
input.setAttribute('name', 'user[theme_id]');
input.setAttribute('type', 'radio');
input.setAttribute('value', themeId.toString());
input.setAttribute('checked', 'checked');
return input;
}
function createForm(themeInput = createThemeInput()) {
const form = document.createElement('form');
form.setAttribute('url', expectedUrl);
form.setAttribute('method', 'put');
form.appendChild(themeInput);
return form;
}
function setupBody() {
const div = document.createElement('div');
div.classList.add('container-fluid');
document.body.appendChild(div);
document.body.classList.add('content-wrapper');
}
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
}); });
...@@ -84,30 +117,15 @@ describe('ProfilePreferences component', () => { ...@@ -84,30 +117,15 @@ describe('ProfilePreferences component', () => {
let form; let form;
beforeEach(() => { beforeEach(() => {
const div = document.createElement('div'); setupBody();
div.classList.add('container-fluid'); form = createForm();
document.body.appendChild(div);
document.body.classList.add('content-wrapper');
form = document.createElement('form');
form.setAttribute('url', expectedUrl);
form.setAttribute('method', 'put');
const input = document.createElement('input');
input.setAttribute('name', 'user[theme_id]');
input.setAttribute('type', 'radio');
input.setAttribute('value', '1');
input.setAttribute('checked', 'checked');
form.appendChild(input);
wrapper = createComponent({ provide: { formEl: form }, attachTo: document.body }); wrapper = createComponent({ provide: { formEl: form }, attachTo: document.body });
const beforeSendEvent = new CustomEvent('ajax:beforeSend'); const beforeSendEvent = new CustomEvent('ajax:beforeSend');
form.dispatchEvent(beforeSendEvent); form.dispatchEvent(beforeSendEvent);
}); });
it('disables the submit button', async () => { it('disables the submit button', async () => {
await wrapper.vm.$nextTick(); await nextTick();
const button = findSubmitButton(); const button = findSubmitButton();
expect(button.props('disabled')).toBe(true); expect(button.props('disabled')).toBe(true);
}); });
...@@ -116,7 +134,7 @@ describe('ProfilePreferences component', () => { ...@@ -116,7 +134,7 @@ describe('ProfilePreferences component', () => {
const successEvent = new CustomEvent('ajax:success'); const successEvent = new CustomEvent('ajax:success');
form.dispatchEvent(successEvent); form.dispatchEvent(successEvent);
await wrapper.vm.$nextTick(); await nextTick();
const button = findSubmitButton(); const button = findSubmitButton();
expect(button.props('disabled')).toBe(false); expect(button.props('disabled')).toBe(false);
}); });
...@@ -125,7 +143,7 @@ describe('ProfilePreferences component', () => { ...@@ -125,7 +143,7 @@ describe('ProfilePreferences component', () => {
const errorEvent = new CustomEvent('ajax:error'); const errorEvent = new CustomEvent('ajax:error');
form.dispatchEvent(errorEvent); form.dispatchEvent(errorEvent);
await wrapper.vm.$nextTick(); await nextTick();
const button = findSubmitButton(); const button = findSubmitButton();
expect(button.props('disabled')).toBe(false); expect(button.props('disabled')).toBe(false);
}); });
...@@ -160,4 +178,89 @@ describe('ProfilePreferences component', () => { ...@@ -160,4 +178,89 @@ describe('ProfilePreferences component', () => {
expect(findFlashError().innerText.trim()).toEqual(message); expect(findFlashError().innerText.trim()).toEqual(message);
}); });
}); });
describe('theme changes', () => {
const { location } = window;
let themeInput;
let form;
function setupWrapper() {
wrapper = createComponent({ provide: { formEl: form }, attachTo: document.body });
}
function selectThemeId(themeId) {
themeInput.setAttribute('value', themeId.toString());
}
function dispatchBeforeSendEvent() {
const beforeSendEvent = new CustomEvent('ajax:beforeSend');
form.dispatchEvent(beforeSendEvent);
}
function dispatchSuccessEvent() {
const successEvent = new CustomEvent('ajax:success');
form.dispatchEvent(successEvent);
}
beforeAll(() => {
delete window.location;
window.location = {
...location,
reload: jest.fn(),
};
});
afterAll(() => {
window.location = location;
});
beforeEach(() => {
setupBody();
themeInput = createThemeInput();
form = createForm(themeInput);
});
it('reloads the page when switching from light to dark mode', async () => {
selectThemeId(lightModeThemeId1);
setupWrapper();
selectThemeId(darkModeThemeId);
dispatchBeforeSendEvent();
await nextTick();
dispatchSuccessEvent();
await nextTick();
expect(window.location.reload).toHaveBeenCalledTimes(1);
});
it('reloads the page when switching from dark to light mode', async () => {
selectThemeId(darkModeThemeId);
setupWrapper();
selectThemeId(lightModeThemeId1);
dispatchBeforeSendEvent();
await nextTick();
dispatchSuccessEvent();
await nextTick();
expect(window.location.reload).toHaveBeenCalledTimes(1);
});
it('does not reload the page when switching between light mode themes', async () => {
selectThemeId(lightModeThemeId1);
setupWrapper();
selectThemeId(lightModeThemeId2);
dispatchBeforeSendEvent();
await nextTick();
dispatchSuccessEvent();
await nextTick();
expect(window.location.reload).not.toHaveBeenCalled();
});
});
}); });
...@@ -18,3 +18,15 @@ export const userFields = { ...@@ -18,3 +18,15 @@ export const userFields = {
}; };
export const bodyClasses = 'ui-light-indigo ui-light gl-dark'; export const bodyClasses = 'ui-light-indigo ui-light gl-dark';
export const themes = [
{ id: 1, css_class: 'foo' },
{ id: 2, css_class: 'bar' },
{ id: 3, css_class: 'gl-dark' },
];
export const lightModeThemeId1 = 1;
export const lightModeThemeId2 = 2;
export const darkModeThemeId = 3;
...@@ -57,14 +57,12 @@ RSpec.describe Gitlab::TreeSummary do ...@@ -57,14 +57,12 @@ RSpec.describe Gitlab::TreeSummary do
context 'with caching', :use_clean_rails_memory_store_caching do context 'with caching', :use_clean_rails_memory_store_caching do
subject { Rails.cache.fetch(key) } subject { Rails.cache.fetch(key) }
before do
summarized
end
context 'Repository tree cache' do context 'Repository tree cache' do
let(:key) { ['projects', project.id, 'content', commit.id, path] } let(:key) { ['projects', project.id, 'content', commit.id, path] }
it 'creates a cache for repository content' do it 'creates a cache for repository content' do
summarized
is_expected.to eq([{ file_name: 'a.txt', type: :blob }]) is_expected.to eq([{ file_name: 'a.txt', type: :blob }])
end end
end end
...@@ -72,11 +70,34 @@ RSpec.describe Gitlab::TreeSummary do ...@@ -72,11 +70,34 @@ RSpec.describe Gitlab::TreeSummary do
context 'Commits list cache' do context 'Commits list cache' do
let(:offset) { 0 } let(:offset) { 0 }
let(:limit) { 25 } let(:limit) { 25 }
let(:key) { ['projects', project.id, 'last_commits_list', commit.id, path, offset, limit] } let(:key) { ['projects', project.id, 'last_commits', commit.id, path, offset, limit] }
it 'creates a cache for commits list' do it 'creates a cache for commits list' do
summarized
is_expected.to eq('a.txt' => commit.to_hash) is_expected.to eq('a.txt' => commit.to_hash)
end end
context 'when commit has a very long message' do
before do
repo.create_file(
project.creator,
'long.txt',
'',
message: message,
branch_name: project.default_branch_or_master
)
end
let(:message) { 'a' * 1025 }
let(:expected_message) { message[0...1021] + '...' }
it 'truncates commit message to 1 kilobyte' do
summarized
is_expected.to include('long.txt' => a_hash_including(message: expected_message))
end
end
end 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