mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-23 00:47:51 +00:00
Update from merge request
This commit is contained in:
@ -1 +1 @@
|
||||
47335750c521ec469a5074962fb28b48b9dc06a6
|
||||
bacb63191f1ba77f6be796e72e9b5bff68fde31d
|
||||
|
@ -303,7 +303,7 @@ export default {
|
||||
is-collapsible
|
||||
:collapsed="isCollapsed"
|
||||
persist-collapsed-state
|
||||
class="!gl-mt-5"
|
||||
class="!gl-mt-5 !gl-overflow-hidden"
|
||||
:body-class="{ '!gl-m-0 !gl-p-0': data.count || isPreview }"
|
||||
@collapsed="isCollapsed = true"
|
||||
@expanded="isCollapsed = false"
|
||||
@ -323,7 +323,7 @@ export default {
|
||||
<component :is="previewPresenter.component" v-else-if="previewPresenter && !hasError" />
|
||||
<div
|
||||
v-if="data.count && data.nodes.length < data.count"
|
||||
class="gl-border-t gl-border-section gl-p-3"
|
||||
class="glql-load-more gl-border-t gl-border-section gl-p-3"
|
||||
>
|
||||
<glql-pagination :count="data.nodes.length" :total-count="data.count" />
|
||||
</div>
|
||||
@ -331,11 +331,8 @@ export default {
|
||||
<template v-if="showEmptyState" #empty>
|
||||
{{ __('No data found for this query.') }}
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<glql-footnote />
|
||||
</template>
|
||||
</crud-component>
|
||||
<glql-footnote v-if="!isCollapsed" />
|
||||
</template>
|
||||
<div v-else-if="hasError" class="markdown-code-block gl-relative">
|
||||
<pre :class="preClasses"><code>{{ query }}</code></pre>
|
||||
|
@ -21,7 +21,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle">
|
||||
<div class="gl-mb-5 gl-mt-2 gl-flex gl-items-center gl-gap-1 gl-text-sm gl-text-subtle">
|
||||
<gl-icon class="gl-mb-1 gl-mr-1" :size="12" name="tanuki" />
|
||||
<gl-sprintf :message="__('%{linkStart}Embedded view%{linkEnd} powered by GLQL')">
|
||||
<template #link="{ content }">
|
||||
|
@ -26,20 +26,3 @@ export const PANEL_POPOVER_DELAY = {
|
||||
};
|
||||
|
||||
export const CURSOR_GRABBING_CLASS = '!gl-cursor-grabbing';
|
||||
|
||||
export const NEW_DASHBOARD_SLUG = 'new';
|
||||
|
||||
export const CATEGORY_SINGLE_STATS = 'singleStats';
|
||||
export const CATEGORY_TABLES = 'tables';
|
||||
export const CATEGORY_CHARTS = 'charts';
|
||||
|
||||
export const DASHBOARD_STATUS_BETA = 'beta';
|
||||
export const DASHBOARD_STATUS_EXPERIMENT = 'experiment';
|
||||
|
||||
export const DASHBOARD_SCHEMA_VERSION = '2';
|
||||
export const VISUALIZATION_TYPE_DATA_TABLE = 'DataTable';
|
||||
export const VISUALIZATION_TYPE_LINE_CHART = 'LineChart';
|
||||
export const VISUALIZATION_TYPE_COLUMN_CHART = 'ColumnChart';
|
||||
export const VISUALIZATION_TYPE_SINGLE_STAT = 'SingleStat';
|
||||
|
||||
export const EVENT_LABEL_VIEWED_DASHBOARD_DESIGNER = 'user_viewed_dashboard_designer';
|
||||
|
@ -8,14 +8,14 @@ import { s__, __ } from '~/locale';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { EVENT_LABEL_VIEWED_DASHBOARD_DESIGNER } from './constants';
|
||||
import GridstackWrapper from './gridstack_wrapper.vue';
|
||||
import AvailableVisualizationsDrawer from './dashboard_editor/available_visualizations_drawer.vue';
|
||||
import { EVENT_LABEL_VIEWED_DASHBOARD_DESIGNER } from 'ee/analytics/analytics_dashboards/constants';
|
||||
import {
|
||||
getDashboardConfig,
|
||||
availableVisualizationsValidator,
|
||||
createNewVisualizationPanel,
|
||||
} from './utils';
|
||||
} from 'ee/analytics/analytics_dashboards/utils';
|
||||
import GridstackWrapper from './gridstack_wrapper.vue';
|
||||
import AvailableVisualizationsDrawer from './dashboard_editor/available_visualizations_drawer.vue';
|
||||
|
||||
export default {
|
||||
name: 'CustomizableDashboard',
|
||||
|
@ -5,8 +5,12 @@ import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
|
||||
import { s__ } from '~/locale';
|
||||
import { toggleArrayItem } from '~/lib/utils/array_utility';
|
||||
import { getVisualizationCategory } from '../utils';
|
||||
import { CATEGORY_SINGLE_STATS, CATEGORY_CHARTS, CATEGORY_TABLES } from '../constants';
|
||||
import {
|
||||
CATEGORY_SINGLE_STATS,
|
||||
CATEGORY_CHARTS,
|
||||
CATEGORY_TABLES,
|
||||
} from 'ee/analytics/analytics_dashboards/constants';
|
||||
import { getVisualizationCategory } from 'ee/analytics/analytics_dashboards/utils';
|
||||
|
||||
export default {
|
||||
name: 'AvailableVisualizatiosnDrawer',
|
||||
|
@ -1,17 +1,4 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
|
||||
import { humanize } from '~/lib/utils/text_utility';
|
||||
import { cloneWithoutReferences } from '~/lib/utils/common_utils';
|
||||
|
||||
import {
|
||||
DASHBOARD_SCHEMA_VERSION,
|
||||
VISUALIZATION_TYPE_DATA_TABLE,
|
||||
VISUALIZATION_TYPE_SINGLE_STAT,
|
||||
CATEGORY_SINGLE_STATS,
|
||||
CATEGORY_CHARTS,
|
||||
CATEGORY_TABLES,
|
||||
} from './constants';
|
||||
|
||||
export const isEmptyPanelData = (visualizationType, data) => {
|
||||
if (visualizationType === 'SingleStat') {
|
||||
@ -22,52 +9,6 @@ export const isEmptyPanelData = (visualizationType, data) => {
|
||||
return isEmpty(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validator for the availableVisualizations property
|
||||
*/
|
||||
export const availableVisualizationsValidator = ({ loading, hasError, visualizations }) => {
|
||||
return (
|
||||
typeof loading === 'boolean' && typeof hasError === 'boolean' && Array.isArray(visualizations)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the category key for visualizations by their type. Default is "charts".
|
||||
*/
|
||||
export const getVisualizationCategory = (visualization) => {
|
||||
if (visualization.type === VISUALIZATION_TYPE_SINGLE_STAT) {
|
||||
return CATEGORY_SINGLE_STATS;
|
||||
}
|
||||
if (visualization.type === VISUALIZATION_TYPE_DATA_TABLE) {
|
||||
return CATEGORY_TABLES;
|
||||
}
|
||||
return CATEGORY_CHARTS;
|
||||
};
|
||||
|
||||
export const getUniquePanelId = () => uniqueId('panel-');
|
||||
|
||||
/**
|
||||
* Maps a full hydrated dashboard (including GraphQL __typenames, and full visualization definitions) into a slimmed down version that complies with the dashboard schema definition
|
||||
*/
|
||||
export const getDashboardConfig = (hydratedDashboard) => {
|
||||
const { __typename: dashboardTypename, userDefined, slug, ...dashboardRest } = hydratedDashboard;
|
||||
|
||||
return {
|
||||
...dashboardRest,
|
||||
version: DASHBOARD_SCHEMA_VERSION,
|
||||
panels: hydratedDashboard.panels.map((panel) => {
|
||||
const { __typename: panelTypename, id, ...panelRest } = panel;
|
||||
const { __typename: visualizationTypename, ...visualizationRest } = panel.visualization;
|
||||
|
||||
return {
|
||||
...panelRest,
|
||||
queryOverrides: panel.queryOverrides ?? {},
|
||||
visualization: visualizationRest,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const filterUndefinedValues = (obj) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined));
|
||||
@ -98,18 +39,6 @@ export const parsePanelToGridItem = ({
|
||||
},
|
||||
});
|
||||
|
||||
export const createNewVisualizationPanel = (visualization) => ({
|
||||
id: getUniquePanelId(),
|
||||
title: humanize(visualization.slug),
|
||||
gridAttributes: {
|
||||
width: 4,
|
||||
height: 3,
|
||||
},
|
||||
queryOverrides: {},
|
||||
options: {},
|
||||
visualization: cloneWithoutReferences({ ...visualization, errors: null }),
|
||||
});
|
||||
|
||||
export const dashboardConfigValidator = (config) => {
|
||||
if (config.panels) {
|
||||
if (!Array.isArray(config.panels)) return false;
|
||||
|
@ -208,6 +208,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Fix border bleed.
|
||||
.gl-table-shadow:not(:has(+ .glql-load-more)) {
|
||||
clip-path: inset(0 round 0 0 1px 1px);
|
||||
}
|
||||
|
||||
.gl-table-shadow {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
@ -111,9 +111,9 @@ module Mutations
|
||||
end
|
||||
end
|
||||
|
||||
def find_parent_by_full_path(full_path)
|
||||
def find_parent_by_full_path(full_path, model = ::Project)
|
||||
# Note: Group support is added in the EE module. For CE, we only support bulk edit for projects
|
||||
::Gitlab::Graphql::Loaders::FullPathModelLoader.new(::Project, full_path).find.sync
|
||||
::Gitlab::Graphql::Loaders::FullPathModelLoader.new(model, full_path).find.sync
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -277,6 +277,8 @@ module Gitlab
|
||||
strategy.increment(cache_key, expiry)
|
||||
end
|
||||
|
||||
return false if value.nil?
|
||||
|
||||
report_metrics(key, value, threshold_value, peek)
|
||||
|
||||
value > threshold_value
|
||||
|
@ -17,7 +17,7 @@ module Gitlab
|
||||
private
|
||||
|
||||
def with_redis(&block)
|
||||
::Gitlab::Redis::RateLimiting.with(&block) # rubocop: disable CodeReuse/ActiveRecord
|
||||
::Gitlab::Redis::RateLimiting.with_suppressed_errors(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -40,11 +40,7 @@ module Gitlab
|
||||
private
|
||||
|
||||
def with(&block)
|
||||
Gitlab::Redis::RateLimiting.with(&block)
|
||||
rescue ::Redis::BaseConnectionError
|
||||
# Do not raise an error if we cannot connect to Redis. If
|
||||
# Redis::RateLimiting is unavailable it should not take the site down.
|
||||
nil
|
||||
Gitlab::Redis::RateLimiting.with_suppressed_errors(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -38,15 +38,7 @@ module Gitlab
|
||||
private
|
||||
|
||||
def with(&block)
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
Gitlab::Redis::RateLimiting.with(&block)
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
rescue ::Redis::BaseConnectionError
|
||||
# Following the example of
|
||||
# https://github.com/rack/rack-attack/blob/v6.6.1/lib/rack/attack/store_proxy/redis_proxy.rb#L61-L65,
|
||||
# do not raise an error if we cannot connect to Redis. If
|
||||
# Redis::RateLimiting is unavailable it should not take the site down.
|
||||
nil
|
||||
Gitlab::Redis::RateLimiting.with_suppressed_errors(&block)
|
||||
end
|
||||
|
||||
def namespace(key)
|
||||
|
@ -8,6 +8,13 @@ module Gitlab
|
||||
def config_fallback
|
||||
Cache
|
||||
end
|
||||
|
||||
# Rescue Redis errors so we do not take the site down when the rate limiting instance is down
|
||||
def with_suppressed_errors(&block)
|
||||
with(&block)
|
||||
rescue ::Redis::BaseError, ::RedisClient::Error => e
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ import AvailableVisualizationsDrawer from '~/vue_shared/components/customizable_
|
||||
import {
|
||||
EVENT_LABEL_VIEWED_DASHBOARD_DESIGNER,
|
||||
DASHBOARD_SCHEMA_VERSION,
|
||||
} from '~/vue_shared/components/customizable_dashboard/constants';
|
||||
} from 'ee/analytics/analytics_dashboards/constants';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
|
@ -10,12 +10,10 @@ import {
|
||||
GRIDSTACK_CELL_HEIGHT,
|
||||
GRIDSTACK_MIN_ROW,
|
||||
} from '~/vue_shared/components/customizable_dashboard/constants';
|
||||
import { createNewVisualizationPanel } from 'ee/analytics/analytics_dashboards/utils';
|
||||
import { loadCSSFile } from '~/lib/utils/css_utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
parsePanelToGridItem,
|
||||
createNewVisualizationPanel,
|
||||
} from '~/vue_shared/components/customizable_dashboard/utils';
|
||||
import { parsePanelToGridItem } from '~/vue_shared/components/customizable_dashboard/utils';
|
||||
import { dashboard, builtinDashboard } from './mock_data';
|
||||
|
||||
const mockGridSetStatic = jest.fn();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getUniquePanelId } from '~/vue_shared/components/customizable_dashboard/utils';
|
||||
import { getUniquePanelId } from 'ee/analytics/analytics_dashboards/utils';
|
||||
|
||||
export const createVisualization = () => ({
|
||||
version: 1,
|
||||
|
@ -1,39 +1,10 @@
|
||||
import {
|
||||
isEmptyPanelData,
|
||||
availableVisualizationsValidator,
|
||||
getDashboardConfig,
|
||||
getVisualizationCategory,
|
||||
parsePanelToGridItem,
|
||||
createNewVisualizationPanel,
|
||||
dashboardConfigValidator,
|
||||
} from '~/vue_shared/components/customizable_dashboard/utils';
|
||||
|
||||
import {
|
||||
CATEGORY_SINGLE_STATS,
|
||||
CATEGORY_CHARTS,
|
||||
CATEGORY_TABLES,
|
||||
DASHBOARD_SCHEMA_VERSION,
|
||||
} from '~/vue_shared/components/customizable_dashboard/constants';
|
||||
|
||||
import { dashboard, mockPanel, createVisualization } from './mock_data';
|
||||
|
||||
describe('#createNewVisualizationPanel', () => {
|
||||
it('returns the expected object', () => {
|
||||
const visualization = createVisualization();
|
||||
expect(createNewVisualizationPanel(visualization)).toMatchObject({
|
||||
visualization: {
|
||||
...visualization,
|
||||
errors: null,
|
||||
},
|
||||
title: 'Test visualization',
|
||||
gridAttributes: {
|
||||
width: 4,
|
||||
height: 3,
|
||||
},
|
||||
options: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
import { dashboard, mockPanel } from './mock_data';
|
||||
|
||||
describe('isEmptyPanelData', () => {
|
||||
it.each`
|
||||
@ -51,84 +22,6 @@ describe('isEmptyPanelData', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('availableVisualizationsValidator', () => {
|
||||
it('returns true when the object contains all properties', () => {
|
||||
const result = availableVisualizationsValidator({
|
||||
loading: false,
|
||||
hasError: false,
|
||||
visualizations: [],
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ visualizations: [] },
|
||||
{ hasError: false },
|
||||
{ loading: true },
|
||||
{ loading: true, hasError: false },
|
||||
])('returns false when the object does not contain all properties', (testCase) => {
|
||||
const result = availableVisualizationsValidator(testCase);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDashboardConfig', () => {
|
||||
it('maps dashboard to expected value', () => {
|
||||
const result = getDashboardConfig(dashboard);
|
||||
const visualization = createVisualization();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 'analytics_overview',
|
||||
version: DASHBOARD_SCHEMA_VERSION,
|
||||
panels: [
|
||||
{
|
||||
gridAttributes: {
|
||||
height: 3,
|
||||
width: 3,
|
||||
},
|
||||
queryOverrides: {},
|
||||
title: 'Test A',
|
||||
visualization,
|
||||
},
|
||||
{
|
||||
gridAttributes: {
|
||||
height: 4,
|
||||
width: 2,
|
||||
},
|
||||
queryOverrides: {
|
||||
limit: 200,
|
||||
},
|
||||
title: 'Test B',
|
||||
visualization,
|
||||
},
|
||||
],
|
||||
title: 'Analytics Overview',
|
||||
status: null,
|
||||
errors: null,
|
||||
});
|
||||
});
|
||||
|
||||
['userDefined', 'slug'].forEach((omitted) => {
|
||||
it(`omits "${omitted}" dashboard property`, () => {
|
||||
const result = getDashboardConfig(dashboard);
|
||||
|
||||
expect(result[omitted]).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVisualizationCategory', () => {
|
||||
it.each`
|
||||
category | type
|
||||
${CATEGORY_SINGLE_STATS} | ${'SingleStat'}
|
||||
${CATEGORY_TABLES} | ${'DataTable'}
|
||||
${CATEGORY_CHARTS} | ${'LineChart'}
|
||||
${CATEGORY_CHARTS} | ${'FooBar'}
|
||||
`('returns $category when the visualization type is $type', ({ category, type }) => {
|
||||
expect(getVisualizationCategory({ type })).toBe(category);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsePanelToGridItem', () => {
|
||||
it('parses all panel configs to GridStack format', () => {
|
||||
const { gridAttributes, ...rest } = mockPanel;
|
||||
|
@ -28,6 +28,21 @@ RSpec.describe Gitlab::ApplicationRateLimiter, :clean_gitlab_redis_rate_limiting
|
||||
end
|
||||
|
||||
describe '.throttled?' do
|
||||
context 'when redis is unavailable' do
|
||||
before do
|
||||
broken_redis = Redis.new(
|
||||
url: 'redis://127.0.0.0:0',
|
||||
custom: { instrumentation_class: Gitlab::Redis::RateLimiting.instrumentation_class }
|
||||
)
|
||||
allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(broken_redis)
|
||||
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject.throttled?(:test_action, scope: [user])).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the key is invalid' do
|
||||
context 'is provided as a Symbol' do
|
||||
context 'but is not defined in the rate_limits Hash' do
|
||||
|
@ -9,6 +9,8 @@ RSpec.describe Gitlab::CircuitBreaker::Store, :clean_gitlab_redis_rate_limiting,
|
||||
|
||||
shared_examples 'reliable circuit breaker store method' do
|
||||
it 'does not raise an error when Redis::BaseConnectionError is encountered' do
|
||||
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
|
||||
allow(Gitlab::Redis::RateLimiting)
|
||||
.to receive(:with)
|
||||
.and_raise(Redis::BaseConnectionError)
|
||||
|
@ -105,6 +105,7 @@ RSpec.describe Gitlab::RackAttack::Store, :clean_gitlab_redis_rate_limiting, fea
|
||||
custom: { instrumentation_class: Gitlab::Redis::RateLimiting.instrumentation_class }
|
||||
)
|
||||
allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(broken_redis)
|
||||
allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
end
|
||||
|
||||
it { expect(subject).to eq(nil) }
|
||||
|
@ -4,4 +4,57 @@ require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Redis::RateLimiting do
|
||||
include_examples "redis_new_instance_shared_examples", 'rate_limiting', Gitlab::Redis::Cache
|
||||
|
||||
describe '.with_suppressed_errors' do
|
||||
subject(:ping) { described_class.with_suppressed_errors(&:ping) }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:with).and_yield(redis)
|
||||
end
|
||||
|
||||
context 'when using Redis' do
|
||||
let(:redis) { described_class.send(:init_redis, { url: 'redis://127.0.0.0:0' }) }
|
||||
|
||||
it 'tracks the error and returns nil' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
.with(a_kind_of(::Redis::CannotConnectError))
|
||||
|
||||
expect(ping).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using RedisCluster' do
|
||||
let(:redis) do
|
||||
described_class.send(:init_redis, {
|
||||
nodes: [
|
||||
{ host: '127.0.0.0', port: 0, db: 1 },
|
||||
{ host: '127.0.0.0', port: 0, db: 2 },
|
||||
{ host: '127.0.0.0', port: 0, db: 3 }
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
it 'tracks the error and returns nil' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
.with(a_kind_of(::Redis::Cluster::InitialSetupError))
|
||||
|
||||
expect(ping).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a RedisClient exception' do
|
||||
let(:redis) { instance_double(Redis) }
|
||||
|
||||
before do
|
||||
allow(redis).to receive(:ping).and_raise(::RedisClient::ReadTimeoutError)
|
||||
end
|
||||
|
||||
it 'tracks the error and returns nil' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
.with(a_kind_of(::RedisClient::ReadTimeoutError))
|
||||
|
||||
expect(ping).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -50,9 +50,78 @@ RSpec.shared_examples 'it has loose foreign keys' do
|
||||
model = create(factory_name) # rubocop: disable Rails/SaveBang
|
||||
model_id = model.id
|
||||
|
||||
puts "## LFK Debug: Starting test for #{table_name} with model_id: #{model_id} ##"
|
||||
|
||||
# Check initial state
|
||||
initial_pending_count = deleted_records.status_pending.count
|
||||
initial_processed_count = deleted_records.status_processed.count
|
||||
initial_total_count = deleted_records.count
|
||||
|
||||
puts "## LFK Debug: Initial state - Pending: #{initial_pending_count}, Processed: #{initial_processed_count}, Total: #{initial_total_count} ##"
|
||||
|
||||
# Check for existing records for this model_id (shouldn't exist)
|
||||
existing_records = deleted_records.where(primary_key_value: model_id)
|
||||
puts "## LFK Debug: Existing records for model_id #{model_id}: #{existing_records.count} ##"
|
||||
|
||||
expect { model.delete }.to change { deleted_records.count }.by(1)
|
||||
|
||||
LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection).execute
|
||||
# Check state after deletion
|
||||
after_delete_pending = deleted_records.where(primary_key_value: model_id).status_pending.count
|
||||
after_delete_processed = deleted_records.where(primary_key_value: model_id).status_processed.count
|
||||
total_pending_after_delete = deleted_records.status_pending.count
|
||||
total_processed_after_delete = deleted_records.status_processed.count
|
||||
|
||||
puts "## LFK Debug: After delete - Model #{model_id} Pending: #{after_delete_pending}, Processed: #{after_delete_processed} ##"
|
||||
puts "## LFK Debug: After delete - Total Pending: #{total_pending_after_delete}, Total Processed: #{total_processed_after_delete} ##"
|
||||
|
||||
# Check all pending records across all tables before processing
|
||||
all_pending_records = []
|
||||
Gitlab::Database::LooseForeignKeys.definitions_by_table.each_key do |table|
|
||||
fqtn = "#{connection.current_schema}.#{table}"
|
||||
table_records = LooseForeignKeys::DeletedRecord.where(fully_qualified_table_name: fqtn).status_pending
|
||||
all_pending_records.concat(table_records.to_a)
|
||||
end
|
||||
|
||||
puts "## LFK Debug: Total pending records across all tables before processing: #{all_pending_records.count} ##"
|
||||
puts "## LFK Debug: Tables with pending records: #{all_pending_records.group_by(&:fully_qualified_table_name).transform_values(&:count)} ##"
|
||||
|
||||
# Process records and capture stats
|
||||
start_time = Time.current
|
||||
service = LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection)
|
||||
stats = service.execute
|
||||
processing_time = Time.current - start_time
|
||||
|
||||
puts "## LFK Debug: Processing took #{processing_time} seconds ##"
|
||||
puts "## LFK Debug: Processing stats: #{stats.inspect} ##"
|
||||
|
||||
# Check state after processing
|
||||
after_process_pending = deleted_records.where(primary_key_value: model_id).status_pending.count
|
||||
after_process_processed = deleted_records.where(primary_key_value: model_id).status_processed.count
|
||||
total_pending_after_process = deleted_records.status_pending.count
|
||||
total_processed_after_process = deleted_records.status_processed.count
|
||||
|
||||
puts "## LFK Debug: After processing - Model #{model_id} Pending: #{after_process_pending}, Processed: #{after_process_processed} ##"
|
||||
puts "## LFK Debug: After processing - Total Pending: #{total_pending_after_process}, Total Processed: #{total_processed_after_process} ##"
|
||||
|
||||
# If the test is about to fail, provide additional debugging
|
||||
if after_process_pending != 0 || after_process_processed != 1
|
||||
puts "## LFK Debug: TEST FAILURE IMMINENT - Additional debugging ##"
|
||||
|
||||
# Check if our specific record exists and its status
|
||||
our_record = deleted_records.find_by(primary_key_value: model_id)
|
||||
|
||||
if our_record
|
||||
puts "## LFK Debug: Our record exists - Status: #{our_record.status}, Cleanup attempts: #{our_record.cleanup_attempts}, Consume after: #{our_record.consume_after} ##"
|
||||
puts "## LFK Debug: Our record details: #{our_record.inspect} ##"
|
||||
else
|
||||
puts "## LFK Debug: Our record not found in deleted_records table ##"
|
||||
end
|
||||
|
||||
# Check if there are still pending records for our table
|
||||
table_pending = deleted_records.status_pending
|
||||
puts "## LFK Debug: Remaining pending records for #{table_name}: #{table_pending.count} ##"
|
||||
puts "## LFK Debug: Pending record IDs: #{table_pending.pluck(:primary_key_value)} ##" if table_pending.any?
|
||||
end
|
||||
|
||||
expect(deleted_records.where(primary_key_value: model_id).status_pending.count).to eq(0)
|
||||
expect(deleted_records.where(primary_key_value: model_id).status_processed.count).to eq(1)
|
||||
|
Reference in New Issue
Block a user