mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-23 13:46:45 +02:00
Fix/variant hashing pg10compat (#5218)
Co-authored-by: Ivar Conradi Østhus <ivar@getunleash.io> Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com> Co-authored-by: Simon Hornby <liquidwicked64@gmail.com>
This commit is contained in:
parent
1e613d8224
commit
264b32be79
@ -53,8 +53,6 @@ const UsersList = () => {
|
||||
const [delUser, setDelUser] = useState<IUser>();
|
||||
const { planUsers, isBillingUsers } = useUsersPlan(users);
|
||||
|
||||
const accessOverviewEnabled = useUiFlag('accessOverview');
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
@ -271,26 +269,15 @@ const UsersList = () => {
|
||||
onChange={setSearchValue}
|
||||
/>
|
||||
<PageHeader.Divider />
|
||||
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
isEnterprise() &&
|
||||
Boolean(accessOverviewEnabled)
|
||||
}
|
||||
show={() => (
|
||||
<>
|
||||
<Tooltip
|
||||
title='Exports user access information'
|
||||
arrow
|
||||
describeChild
|
||||
>
|
||||
<IconButton onClick={downloadCSV}>
|
||||
<Download />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Tooltip
|
||||
title='Exports user access information'
|
||||
arrow
|
||||
describeChild
|
||||
>
|
||||
<IconButton onClick={downloadCSV}>
|
||||
<Download />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
|
@ -65,7 +65,6 @@ export type UiFlags = {
|
||||
doraMetrics?: boolean;
|
||||
variantTypeNumber?: boolean;
|
||||
privateProjects?: boolean;
|
||||
accessOverview?: boolean;
|
||||
dependentFeatures?: boolean;
|
||||
banners?: boolean;
|
||||
disableEnvsOnRevive?: boolean;
|
||||
|
@ -139,7 +139,6 @@
|
||||
"knex": "^2.5.1",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"log4js": "^6.0.0",
|
||||
"make-fetch-happen": "^11.0.0",
|
||||
|
@ -71,7 +71,6 @@ exports[`should create default config 1`] = `
|
||||
},
|
||||
"flagResolver": FlagResolver {
|
||||
"experiments": {
|
||||
"accessOverview": false,
|
||||
"anonymiseEventLog": false,
|
||||
"banners": false,
|
||||
"caseInsensitiveInOperators": false,
|
||||
|
@ -1,67 +0,0 @@
|
||||
import sortBy from 'lodash.sortby';
|
||||
|
||||
interface Difference {
|
||||
index: (string | number)[];
|
||||
reason: string;
|
||||
valueA: any;
|
||||
valueB: any;
|
||||
}
|
||||
|
||||
export function deepDiff(arr1: any[], arr2: any[]): Difference[] | null {
|
||||
const diff: Difference[] = [];
|
||||
|
||||
function compare(a: any, b: any, parentIndex: (string | number)[]): void {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
diff.push({
|
||||
index: parentIndex,
|
||||
reason: 'Different lengths',
|
||||
valueA: a,
|
||||
valueB: b,
|
||||
});
|
||||
} else {
|
||||
const sortedA = sortBy(a, 'name');
|
||||
const sortedB = sortBy(b, 'name');
|
||||
|
||||
for (let i = 0; i < sortedA.length; i++) {
|
||||
compare(sortedA[i], sortedB[i], parentIndex.concat(i));
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
typeof a === 'object' &&
|
||||
a !== null &&
|
||||
typeof b === 'object' &&
|
||||
b !== null
|
||||
) {
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
|
||||
if (
|
||||
keysA.length !== keysB.length ||
|
||||
!keysA.every((key) => keysB.includes(key))
|
||||
) {
|
||||
diff.push({
|
||||
index: parentIndex,
|
||||
reason: 'Different keys',
|
||||
valueA: a,
|
||||
valueB: b,
|
||||
});
|
||||
} else {
|
||||
for (const key of keysA) {
|
||||
compare(a[key], b[key], parentIndex.concat(key));
|
||||
}
|
||||
}
|
||||
} else if (a !== b) {
|
||||
diff.push({
|
||||
index: parentIndex,
|
||||
reason: 'Different values',
|
||||
valueA: a,
|
||||
valueB: b,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
compare(arr1, arr2, []);
|
||||
|
||||
return diff.length > 0 ? diff : null;
|
||||
}
|
@ -102,8 +102,6 @@ import { IPrivateProjectChecker } from '../private-project/privateProjectChecker
|
||||
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
|
||||
import EventService from '../../services/event-service';
|
||||
import { DependentFeaturesService } from '../dependent-features/dependent-features-service';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { deepDiff } from './deep-diff';
|
||||
|
||||
interface IFeatureContext {
|
||||
featureName: string;
|
||||
@ -1058,22 +1056,6 @@ class FeatureToggleService {
|
||||
await this.featureToggleStore.getPlaygroundFeatures(query),
|
||||
]);
|
||||
|
||||
const equal = isEqual(
|
||||
featuresFromClientStore,
|
||||
featuresFromFeatureToggleStore,
|
||||
);
|
||||
|
||||
if (!equal) {
|
||||
const difference = deepDiff(
|
||||
featuresFromClientStore,
|
||||
featuresFromFeatureToggleStore,
|
||||
);
|
||||
this.logger.warn(
|
||||
'getPlaygroundFeatures: features from client-feature-toggle-store is not equal to features from feature-toggle-store',
|
||||
difference,
|
||||
);
|
||||
}
|
||||
|
||||
const features = this.flagResolver.isEnabled('separateAdminClientApi')
|
||||
? featuresFromFeatureToggleStore
|
||||
: featuresFromClientStore;
|
||||
@ -1109,22 +1091,6 @@ class FeatureToggleService {
|
||||
),
|
||||
]);
|
||||
|
||||
const equal = isEqual(
|
||||
featuresFromClientStore,
|
||||
featuresFromFeatureToggleStore,
|
||||
);
|
||||
|
||||
if (!equal) {
|
||||
const difference = deepDiff(
|
||||
featuresFromClientStore,
|
||||
featuresFromFeatureToggleStore,
|
||||
);
|
||||
this.logger.warn(
|
||||
'getFeatureToggles: features from client-feature-toggle-store is not equal to features from feature-toggle-store diff',
|
||||
difference,
|
||||
);
|
||||
}
|
||||
|
||||
const features = this.flagResolver.isEnabled('separateAdminClientApi')
|
||||
? featuresFromFeatureToggleStore
|
||||
: featuresFromClientStore;
|
||||
|
@ -342,11 +342,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
|
||||
|
||||
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
|
||||
query.leftJoin(
|
||||
'last_seen_at_metrics',
|
||||
'last_seen_at_metrics.environment',
|
||||
'features_view.environment_name',
|
||||
);
|
||||
query.leftJoin('last_seen_at_metrics', function () {
|
||||
this.on(
|
||||
'last_seen_at_metrics.environment',
|
||||
'=',
|
||||
'features_view.environment_name',
|
||||
).andOn(
|
||||
'last_seen_at_metrics.feature_name',
|
||||
'=',
|
||||
'features_view.name',
|
||||
);
|
||||
});
|
||||
// Override feature view for now
|
||||
selectColumns.push(
|
||||
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
||||
|
@ -1,79 +0,0 @@
|
||||
import { deepDiff } from '../deep-diff'; // Import the deepDiff function
|
||||
|
||||
describe('deepDiff', () => {
|
||||
test('should sort arrays by name before comparing', () => {
|
||||
// Define two arrays that are identical except for the order of elements
|
||||
const array1 = [
|
||||
{ name: 'b', value: 2 },
|
||||
{ name: 'a', value: 1 },
|
||||
];
|
||||
const array2 = [
|
||||
{ name: 'a', value: 1 },
|
||||
{ name: 'b', value: 2 },
|
||||
];
|
||||
|
||||
// If the function correctly sorts before comparing, there should be no differences
|
||||
const result = deepDiff(array1, array2);
|
||||
|
||||
// Assert that there is no difference
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for equal arrays', () => {
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [1, 2, 3];
|
||||
expect(deepDiff(arr1, arr2)).toBe(null);
|
||||
});
|
||||
|
||||
it('should find differences in arrays with different lengths', () => {
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [1, 2, 3, 4];
|
||||
expect(deepDiff(arr1, arr2)).toEqual([
|
||||
{
|
||||
index: [],
|
||||
reason: 'Different lengths',
|
||||
valueA: arr1,
|
||||
valueB: arr2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find differences in arrays with different values', () => {
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [1, 4, 3];
|
||||
expect(deepDiff(arr1, arr2)).toEqual([
|
||||
{
|
||||
index: [1],
|
||||
reason: 'Different values',
|
||||
valueA: 2,
|
||||
valueB: 4,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find differences in arrays with different keys in objects', () => {
|
||||
const arr1 = [{ a: 1 }, { b: 2 }];
|
||||
const arr2 = [{ a: 1 }, { c: 2 }];
|
||||
expect(deepDiff(arr1, arr2)).toEqual([
|
||||
{
|
||||
index: [1],
|
||||
reason: 'Different keys',
|
||||
valueA: { b: 2 },
|
||||
valueB: { c: 2 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle nested differences in objects', () => {
|
||||
const arr1 = [{ a: { b: 1 } }, { c: { d: 2 } }];
|
||||
const arr2 = [{ a: { b: 1 } }, { c: { d: 3 } }];
|
||||
expect(deepDiff(arr1, arr2)).toEqual([
|
||||
{
|
||||
index: [1, 'c', 'd'],
|
||||
reason: 'Different values',
|
||||
valueA: 2,
|
||||
valueB: 3,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -154,3 +154,53 @@ test('response should include last seen at per environment for multiple environm
|
||||
expect(production.name).toBe('production');
|
||||
expect(production.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
|
||||
});
|
||||
|
||||
test('response should include last seen at per environment correctly for a single toggle /api/admin/project/:projectId/features/:featureName', async () => {
|
||||
const featureName = 'multiple-environment-last-seen-at-single-toggle';
|
||||
await app.createFeature(featureName);
|
||||
await setupLastSeenAtTest(`${featureName}1`);
|
||||
await setupLastSeenAtTest(`${featureName}2`);
|
||||
await setupLastSeenAtTest(`${featureName}3`);
|
||||
await setupLastSeenAtTest(`${featureName}4`);
|
||||
await setupLastSeenAtTest(`${featureName}5`);
|
||||
|
||||
await insertLastSeenAt(
|
||||
featureName,
|
||||
db.rawDatabase,
|
||||
'default',
|
||||
'2023-08-01 12:30:56',
|
||||
);
|
||||
|
||||
await insertLastSeenAt(
|
||||
featureName,
|
||||
db.rawDatabase,
|
||||
'development',
|
||||
'2023-08-01 12:30:56',
|
||||
);
|
||||
|
||||
await insertLastSeenAt(
|
||||
featureName,
|
||||
db.rawDatabase,
|
||||
'production',
|
||||
'2023-08-01 12:30:56',
|
||||
);
|
||||
|
||||
const { body } = await app.request
|
||||
.get(`/api/admin/projects/default/features/${featureName}`)
|
||||
.expect(200);
|
||||
|
||||
expect(body.environments).toMatchObject([
|
||||
{
|
||||
name: 'default',
|
||||
lastSeenAt: '2023-08-01T12:30:56.000Z',
|
||||
},
|
||||
{
|
||||
name: 'development',
|
||||
lastSeenAt: '2023-08-01T12:30:56.000Z',
|
||||
},
|
||||
{
|
||||
name: 'production',
|
||||
lastSeenAt: '2023-08-01T12:30:56.000Z',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -26,7 +26,6 @@ export type IFlagKey =
|
||||
| 'featureNamingPattern'
|
||||
| 'doraMetrics'
|
||||
| 'variantTypeNumber'
|
||||
| 'accessOverview'
|
||||
| 'privateProjects'
|
||||
| 'dependentFeatures'
|
||||
| 'disableMetrics'
|
||||
@ -137,10 +136,6 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_PRIVATE_PROJECTS,
|
||||
false,
|
||||
),
|
||||
accessOverview: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_ACCESS_OVERVIEW,
|
||||
false,
|
||||
),
|
||||
disableMetrics: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
|
||||
false,
|
||||
|
@ -7,7 +7,7 @@ exports.up = function(db, cb) {
|
||||
PRIMARY KEY (day, environment)
|
||||
);
|
||||
|
||||
CREATE FUNCTION unleash_update_stat_environment_changes_counter() RETURNS trigger AS $unleash_update_changes_counter$
|
||||
CREATE OR REPLACE FUNCTION unleash_update_stat_environment_changes_counter() RETURNS trigger AS $unleash_update_changes_counter$
|
||||
BEGIN
|
||||
IF NEW.environment IS NOT NULL THEN
|
||||
INSERT INTO stat_environment_updates(day, environment, updates) SELECT DATE_TRUNC('Day', NEW.created_at), NEW.environment, 1 ON CONFLICT (day, environment) DO UPDATE SET updates = stat_environment_updates.updates + 1;
|
||||
@ -19,7 +19,7 @@ exports.up = function(db, cb) {
|
||||
|
||||
CREATE TRIGGER unleash_update_stat_environment_changes
|
||||
AFTER INSERT ON events
|
||||
FOR EACH ROW EXECUTE FUNCTION unleash_update_stat_environment_changes_counter();
|
||||
FOR EACH ROW EXECUTE PROCEDURE unleash_update_stat_environment_changes_counter();
|
||||
`, cb);
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,6 @@ process.nextTick(async () => {
|
||||
doraMetrics: true,
|
||||
variantTypeNumber: true,
|
||||
privateProjects: true,
|
||||
accessOverview: true,
|
||||
dependentFeatures: true,
|
||||
useLastSeenRefactor: true,
|
||||
disableEnvsOnRevive: true,
|
||||
|
@ -326,10 +326,15 @@ export const insertLastSeenAt = async (
|
||||
environment: string = 'default',
|
||||
date: string = '2023-10-01 12:34:56',
|
||||
): Promise<string> => {
|
||||
await db.raw(`INSERT INTO last_seen_at_metrics (feature_name, environment, last_seen_at)
|
||||
try {
|
||||
await db.raw(`INSERT INTO last_seen_at_metrics (feature_name, environment, last_seen_at)
|
||||
VALUES ('${featureName}', '${environment}', '${date}');`);
|
||||
|
||||
return date;
|
||||
return date;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return Promise.resolve('');
|
||||
}
|
||||
};
|
||||
|
||||
export const insertFeatureEnvironmentsLastSeen = async (
|
||||
|
Loading…
Reference in New Issue
Block a user