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 {
data() {
return {
isSubmitEnabled: true,
darkModeOnCreate: null,
darkModeOnSubmit: null,
};
},
computed: {
......@@ -58,6 +60,7 @@ export default {
this.formEl.addEventListener('ajax:beforeSend', this.handleLoading);
this.formEl.addEventListener('ajax:success', this.handleSuccess);
this.formEl.addEventListener('ajax:error', this.handleError);
this.darkModeOnCreate = this.darkModeSelected();
},
beforeDestroy() {
this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading);
......@@ -65,16 +68,27 @@ export default {
this.formEl.removeEventListener('ajax:error', this.handleError);
},
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() {
this.isSubmitEnabled = false;
this.darkModeOnSubmit = this.darkModeSelected();
},
handleSuccess(customEvent) {
const formData = new FormData(this.formEl);
updateClasses(
this.bodyClasses,
this.applicationThemes[formData.get('user[theme_id]')].css_class,
this.selectedLayout,
);
// Reload the page if the theme has changed from light to dark mode or vice versa
// to correctly load all required styles.
const modeChanged = this.darkModeOnCreate ? !this.darkModeOnSubmit : this.darkModeOnSubmit;
if (modeChanged) {
window.location.reload();
return;
}
updateClasses(this.bodyClasses, this.getSelectedTheme().css_class, this.selectedLayout);
const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } =
customEvent?.detail?.[0] || {};
createFlash({ message, type });
......
......@@ -84,11 +84,11 @@ body {
&.gl-dark {
.logo-text svg {
fill: $gl-text-color;
fill: var(--gl-text-color);
}
.navbar-gitlab {
background-color: $gray-50;
background-color: var(--gray-50);
.navbar-sub-nav,
.navbar-nav {
......@@ -97,8 +97,8 @@ body {
> a:focus,
> button:hover,
> button:focus {
color: $gl-text-color;
background-color: $gray-200;
color: var(--gl-text-color);
background-color: var(--gray-200);
}
}
......@@ -106,21 +106,21 @@ body {
li.dropdown.show {
> a,
> button {
color: $gl-text-color;
background-color: $gray-200;
color: var(--gl-text-color);
background-color: var(--gray-200);
}
}
}
.search {
form {
background-color: $gray-100;
box-shadow: inset 0 0 0 1px $border-color;
background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px var(--border-color);
&:active,
&:hover {
background-color: $gray-100;
box-shadow: inset 0 0 0 1px $blue-200;
background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px var(--blue-200);
}
}
}
......
......@@ -6,5 +6,5 @@
= 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 }
.form-actions
= f.submit _('Save changes'), class: 'btn gl-button btn-success'
= link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-cancel'
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm'
= link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'
......@@ -2,7 +2,7 @@
- if @deploy_keys.any?
%h3.page-title.deploy-keys-title
= _('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.table
%thead
......
......@@ -6,5 +6,5 @@
= 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 }
.form-actions
= f.submit 'Create', class: 'btn gl-button btn-success'
= link_to 'Cancel', admin_deploy_keys_path, class: 'btn gl-button btn-cancel'
= f.submit 'Create', class: 'btn gl-button btn-confirm'
= 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|
end
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.
## Email notifications
Email notifications are available in projects that have been
[configured to create incidents automatically](incidents.md#create-incidents-automatically)
for triggered alerts. Project members with the **Owner** or **Maintainer** roles are
sent an email notification automatically. (This is not configurable.) To optionally
send additional email notifications to project members with the **Developer** role:
Email notifications are available in projects for triggered alerts. Project
members with the **Owner** or **Maintainer** roles have the option to receive
a single email notification for new alerts.
1. Navigate to **Settings > Operations**.
1. Expand the **Incidents** section.
1. In the **Alert Integration** tab, select the **Send a separate email notification to Developers**
check box.
1. In the **Alert Integration** tab, select the checkbox
**Send a single email notification to Owners and Maintainers for new alerts**.
1. Select **Save changes**.
......@@ -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.
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
end
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
repository
.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
commits.transform_values! { |value| Commit.from_hash(value, project) }
......@@ -121,6 +121,12 @@ module Gitlab
resolved_commits[commit.id] ||= commit
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)
Gitlab::Routing.url_helpers.project_commit_path(project, commit)
end
......
......@@ -16,11 +16,11 @@ gitlab:
gitaly:
resources:
requests:
cpu: 1200m
memory: 245M
cpu: 2400m
memory: 1000M
limits:
cpu: 1800m
memory: 367M
cpu: 3600m
memory: 1500M
persistence:
size: 10G
gitlab-exporter:
......@@ -38,10 +38,10 @@ gitlab:
resources:
requests:
cpu: 500m
memory: 25M
memory: 100M
limits:
cpu: 750m
memory: 37.5M
memory: 150M
maxReplicas: 3
hpa:
targetAverageValue: 500m
......@@ -52,10 +52,10 @@ gitlab:
resources:
requests:
cpu: 855m
memory: 1285M
memory: 1927M
limits:
cpu: 1282m
memory: 1927M
memory: 2890M
hpa:
targetAverageValue: 650m
task-runner:
......@@ -138,10 +138,10 @@ postgresql:
resources:
requests:
cpu: 550m
memory: 250M
memory: 1000M
limits:
cpu: 825m
memory: 375M
memory: 1500M
prometheus:
install: false
redis:
......
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
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';
......@@ -14,7 +23,7 @@ describe('ProfilePreferences component', () => {
integrationViews: [],
userFields,
bodyClasses,
themes: [{ id: 1, css_class: 'foo' }],
themes,
profilePreferencesPath: '/update-profile',
formEl: document.createElement('form'),
};
......@@ -49,6 +58,30 @@ describe('ProfilePreferences component', () => {
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(() => {
setFixtures('<div class="flash-container"></div>');
});
......@@ -84,30 +117,15 @@ describe('ProfilePreferences component', () => {
let form;
beforeEach(() => {
const div = document.createElement('div');
div.classList.add('container-fluid');
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);
setupBody();
form = createForm();
wrapper = createComponent({ provide: { formEl: form }, attachTo: document.body });
const beforeSendEvent = new CustomEvent('ajax:beforeSend');
form.dispatchEvent(beforeSendEvent);
});
it('disables the submit button', async () => {
await wrapper.vm.$nextTick();
await nextTick();
const button = findSubmitButton();
expect(button.props('disabled')).toBe(true);
});
......@@ -116,7 +134,7 @@ describe('ProfilePreferences component', () => {
const successEvent = new CustomEvent('ajax:success');
form.dispatchEvent(successEvent);
await wrapper.vm.$nextTick();
await nextTick();
const button = findSubmitButton();
expect(button.props('disabled')).toBe(false);
});
......@@ -125,7 +143,7 @@ describe('ProfilePreferences component', () => {
const errorEvent = new CustomEvent('ajax:error');
form.dispatchEvent(errorEvent);
await wrapper.vm.$nextTick();
await nextTick();
const button = findSubmitButton();
expect(button.props('disabled')).toBe(false);
});
......@@ -160,4 +178,89 @@ describe('ProfilePreferences component', () => {
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 = {
};
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
context 'with caching', :use_clean_rails_memory_store_caching do
subject { Rails.cache.fetch(key) }
before do
summarized
end
context 'Repository tree cache' do
let(:key) { ['projects', project.id, 'content', commit.id, path] }
it 'creates a cache for repository content' do
summarized
is_expected.to eq([{ file_name: 'a.txt', type: :blob }])
end
end
......@@ -72,11 +70,34 @@ RSpec.describe Gitlab::TreeSummary do
context 'Commits list cache' do
let(:offset) { 0 }
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
summarized
is_expected.to eq('a.txt' => commit.to_hash)
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
......
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