Skip to content

Commit

Permalink
Merge pull request #12970 from jerolimov/console-samples
Browse files Browse the repository at this point in the history
ODC-7334: Show ConsoleSamples resources in the samples catalog
  • Loading branch information
openshift-merge-robot committed Jul 20, 2023
2 parents 679abdd + db317c3 commit 504df7d
Show file tree
Hide file tree
Showing 42 changed files with 1,606 additions and 228 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
getGroupVersionKindForModel,
ResourceIcon,
} from '@console/dynamic-plugin-sdk/src/lib-core';
import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { useTelemetry } from '@console/shared/src';
import {
Expand All @@ -19,6 +18,7 @@ import {
SaveStatusProps,
} from '@console/shared/src/components/cluster-configuration';
import { QuickStartModel } from '../../models';
import { getQuickStartNameRef, useQuickStarts } from './utils/useQuickStarts';

type DisabledQuickStartsConsoleConfig = K8sResourceKind & {
spec: {
Expand All @@ -39,7 +39,7 @@ const Item: React.FC<ItemProps> = ({ id, quickStart }) => (
<ResourceIcon groupVersionKind={getGroupVersionKindForModel(QuickStartModel)} />
<div>
<div>{quickStart.spec.displayName || quickStart.metadata.name}</div>
{quickStart.spec.displayName ? <div>{quickStart.metadata.name}</div> : null}
{quickStart.spec.displayName ? <div>{id}</div> : null}
</div>
</>
) : (
Expand All @@ -53,12 +53,7 @@ const QuickStartConfiguration: React.FC<{ readonly: boolean }> = ({ readonly })
const fireTelemetryEvent = useTelemetry();

// All available quick starts
const [allQuickStarts, allQuickStartsLoaded, allQuickStartsError] = useK8sWatchResource<
QuickStart[]
>({
groupVersionKind: getGroupVersionKindForModel(QuickStartModel),
isList: true,
});
const [allQuickStarts, allQuickStartsLoaded, allQuickStartsError] = useQuickStarts(false);

// Current configuration
const [consoleConfig, consoleConfigLoaded, consoleConfigError] = useConsoleOperatorConfig<
Expand All @@ -77,7 +72,7 @@ const QuickStartConfiguration: React.FC<{ readonly: boolean }> = ({ readonly })
return [];
}
return allQuickStarts
.filter((quickStart) => !disabled || !disabled.includes(quickStart.metadata.name))
.filter((quickStart) => !disabled || !disabled.includes(getQuickStartNameRef(quickStart)))
.sort((quickStartA, quickStartB) => {
const displayNameA = quickStartA.spec.displayName || quickStartA.metadata.name;
const displayNameB = quickStartB.spec.displayName || quickStartB.metadata.name;
Expand All @@ -86,7 +81,7 @@ const QuickStartConfiguration: React.FC<{ readonly: boolean }> = ({ readonly })
.map((quickStart) => (
<Item
key={quickStart.metadata.name}
id={quickStart.metadata.name}
id={getQuickStartNameRef(quickStart)}
quickStart={quickStart}
/>
));
Expand All @@ -97,7 +92,7 @@ const QuickStartConfiguration: React.FC<{ readonly: boolean }> = ({ readonly })
}
const quickStartsByName = allQuickStarts.reduce<Record<string, QuickStart>>(
(acc, quickStart) => {
acc[quickStart.metadata.name] = quickStart;
acc[getQuickStartNameRef(quickStart)] = quickStart;
return acc;
},
{},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
import * as React from 'react';
import { QuickStart, isDisabledQuickStart, getDisabledQuickStarts } from '@patternfly/quickstarts';
import { QuickStart } from '@patternfly/quickstarts';
import { QuickStartsLoaderProps } from '@console/dynamic-plugin-sdk/src/api/internal-types';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
import { referenceForModel } from '@console/internal/module/k8s/k8s';
import { QuickStartModel } from '../../../models';
import { useQuickStarts } from '../utils/useQuickStarts';
import QuickStartPermissionChecker from './QuickStartPermissionChecker';

const QuickStartsLoader: React.FC<QuickStartsLoaderProps> = ({ children }) => {
const [quickStarts, quickStartsLoaded] = useK8sWatchResource<QuickStart[]>({
kind: referenceForModel(QuickStartModel),
isList: true,
});

const enabledQuickstarts = React.useMemo(() => {
const disabledQuickStarts = getDisabledQuickStarts();
if (quickStartsLoaded && disabledQuickStarts.length > 0) {
return quickStarts.filter((qs) => !isDisabledQuickStart(qs, disabledQuickStarts));
}
return quickStarts;
}, [quickStarts, quickStartsLoaded]);
const [quickStarts, quickStartsLoaded] = useQuickStarts();

const [allowedQuickStarts, setAllowedQuickStarts] = React.useState<QuickStart[]>([]);
const [permissionsLoaded, setPermissionsLoaded] = React.useState<boolean>(false);
Expand All @@ -27,20 +14,20 @@ const QuickStartsLoader: React.FC<QuickStartsLoaderProps> = ({ children }) => {
const handlePermissionCheck = React.useCallback(
(quickStart, hasPermission) => {
permissionChecks.current[quickStart.metadata.name] = hasPermission;
if (Object.keys(permissionChecks.current).length === enabledQuickstarts.length) {
const filteredQuickStarts = enabledQuickstarts.filter(
if (Object.keys(permissionChecks.current).length === quickStarts.length) {
const filteredQuickStarts = quickStarts.filter(
(quickstart) => permissionChecks.current[quickstart.metadata.name],
);
setAllowedQuickStarts(filteredQuickStarts);
setPermissionsLoaded(true);
}
},
[enabledQuickstarts],
[quickStarts],
);

return (
<>
{enabledQuickstarts.map((quickstart) => {
{quickStarts.map((quickstart) => {
return (
<QuickStartPermissionChecker
key={quickstart.metadata.name}
Expand All @@ -51,7 +38,7 @@ const QuickStartsLoader: React.FC<QuickStartsLoaderProps> = ({ children }) => {
})}
{children(
allowedQuickStarts,
quickStartsLoaded && (enabledQuickstarts.length === 0 || permissionsLoaded),
quickStartsLoaded && (quickStarts.length === 0 || permissionsLoaded),
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { QuickStart } from '@patternfly/quickstarts';

export const quickStartSample: QuickStart = {
apiVersion: 'console.openshift.io/v1',
kind: 'ConsoleQuickStart',
metadata: { name: 'quickstart-sample' },
spec: {
displayName: 'QuickStart sample',
description: '',
icon: '',
},
};

export const anotherQuickStartSample: QuickStart = {
apiVersion: 'console.openshift.io/v1',
kind: 'ConsoleQuickStart',
metadata: { name: 'another-quickstart-sample' },
spec: {
displayName: 'QuickStart sample',
description: '',
icon: '',
},
};

const getPseudoTranslatedQuickStart = (
quickStart: QuickStart,
lang?: string,
country?: string,
): QuickStart => {
return {
...quickStart,
metadata: {
...quickStart.metadata,
name: `${quickStart.metadata.name}${lang ? `-${lang}` : ''}${country ? `-${country}` : ''}`,
labels: {
...quickStart.metadata.labels,
'console.openshift.io/name': quickStart.metadata.name,
'console.openshift.io/lang': lang,
'console.openshift.io/country': country,
},
},
spec: {
...quickStart.spec,
displayName: `${quickStart.spec.displayName}${lang ? ` ${lang}` : ''}${
country ? `-${country}` : ''
}`,
},
};
};

export const translatedQuickStarts: QuickStart[] = [
getPseudoTranslatedQuickStart(quickStartSample),
getPseudoTranslatedQuickStart(quickStartSample, 'en', 'US'),
getPseudoTranslatedQuickStart(quickStartSample, 'EN', 'CA'),
getPseudoTranslatedQuickStart(quickStartSample, 'fr', 'CA'),
getPseudoTranslatedQuickStart(quickStartSample, 'fr'),
getPseudoTranslatedQuickStart(quickStartSample, 'de', 'DE'),
getPseudoTranslatedQuickStart(quickStartSample, 'de', 'AT'),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getBestMatch, groupQuickStartsByName } from '../useQuickStarts';
import {
quickStartSample,
anotherQuickStartSample,
translatedQuickStarts,
} from './useQuickStarts.data';

describe('groupConsoleSamplesByName', () => {
it('should create a single group for one sample without localization labels', () => {
const actual = groupQuickStartsByName([quickStartSample]);
expect(actual).toEqual({
'quickstart-sample': [quickStartSample],
});
});

it('should create a two groups for two different samples without localization labels', () => {
const actual = groupQuickStartsByName([quickStartSample, anotherQuickStartSample]);
expect(actual).toEqual({
'quickstart-sample': [quickStartSample],
'another-quickstart-sample': [anotherQuickStartSample],
});
});

it('should group the translated samples correct', () => {
const actual = groupQuickStartsByName(translatedQuickStarts);

expect(actual).toEqual({ 'quickstart-sample': translatedQuickStarts });
});
});

describe('getBestMatch', () => {
it('should return null for null', () => {
expect(getBestMatch(null, '')).toBe(null);
});

it('should return null for an empty array', () => {
expect(getBestMatch([], 'en')).toBe(null);
});

it('should return the sample with an equal language and country', () => {
// neighter language or country is defined
expect(getBestMatch(translatedQuickStarts, '')).toEqual(translatedQuickStarts[0]);
// default language is preferred, no country
expect(getBestMatch(translatedQuickStarts, 'en')).toEqual(translatedQuickStarts[0]);
// non default language and country
expect(getBestMatch(translatedQuickStarts, 'fr-CA')).toEqual(translatedQuickStarts[3]);
// lowercase input
expect(getBestMatch(translatedQuickStarts, 'fr-ca')).toEqual(translatedQuickStarts[3]);
// uppercase input
expect(getBestMatch(translatedQuickStarts, 'FR-CA')).toEqual(translatedQuickStarts[3]);
// language defined, but not english, no country
expect(getBestMatch(translatedQuickStarts, 'fr')).toEqual(translatedQuickStarts[4]);
});

it('should return the sample with the same language, prefer no country', () => {
expect(getBestMatch(translatedQuickStarts, 'fr-MC')).toEqual(translatedQuickStarts[4]);
});

it('should return the sample with the same language, prefer any country', () => {
expect(getBestMatch(translatedQuickStarts, 'DE-CH')).toEqual(translatedQuickStarts[5]);
});

it('should return the fallback language with the same country', () => {
expect(getBestMatch(translatedQuickStarts, 'NA-CA')).toEqual(translatedQuickStarts[2]);
});

it('should return the fallback language with no country', () => {
expect(getBestMatch(translatedQuickStarts, 'NA-NA')).toEqual(translatedQuickStarts[0]);
});

it('should return the fallback language with any country', () => {
expect(getBestMatch(translatedQuickStarts.slice(1), 'NA-NA')).toEqual(translatedQuickStarts[1]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as React from 'react';
import { QuickStart, getDisabledQuickStarts } from '@patternfly/quickstarts';
import { useTranslation } from 'react-i18next';
import {
WatchK8sResult,
getGroupVersionKindForModel,
} from '@console/dynamic-plugin-sdk/src/lib-core';
import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks/useK8sWatchResource';
import { QuickStartModel } from '../../../models';

const LOCALIZATION_NAME_LABEL = 'console.openshift.io/name';
const LOCALIZATION_LANGUAGE_LABEL = 'console.openshift.io/lang';
const LOCALIZATION_COUNTRY_LABEL = 'console.openshift.io/country';

export const getQuickStartNameRef = (quickStart: QuickStart) =>
quickStart.metadata.labels?.[LOCALIZATION_NAME_LABEL] ||
quickStart.metadata.annotations?.[LOCALIZATION_NAME_LABEL] ||
quickStart.metadata.name;

export const groupQuickStartsByName = (quickStarts: QuickStart[]) => {
return quickStarts.reduce<Record<string, QuickStart[]>>((grouped, quickStart) => {
const name = getQuickStartNameRef(quickStart);
if (!grouped[name]) grouped[name] = [];
grouped[name].push(quickStart);
return grouped;
}, {});
};

/**
* Returns the QuickStart with the best localization match, for the given
* preferred language and preferred country. It prefers a match in this order:
*
* 1. QuickStart language and country are equal to the preferred language and country.
* This includes sample without language (fallbacks to en) and no country.
*
* 2. QuickStart language is equal to the preferred language.
* 1. And the quick starts has no country defined. (eg, select en quick starts is used for en-CA and en-GB)
* 2. Any country is defined. (eg, select en-CA quick starts is used for en-GB)
*
* 3. Fallback to an english quick starts
* (QuickStart language is en OR quick starts language is not defined):
* 1. Same country (use en-CA quick starts if preference is fr-CA)
* 2. No country
* 3. Any country (use en-CA quick starts if preference is en-US)
*/
export const getBestMatch = (quickStarts: QuickStart[], language: string): QuickStart | null => {
if (!quickStarts || !quickStarts.length) {
return null;
}
const preferredLanguage = (language || 'en').split('-')[0].toLowerCase();
const preferredCountry = ((language || '').split('-')[1] || '').toUpperCase();

let sameLanguageWithoutCountry: QuickStart = null;
let sameLanguageWithAnyCountry: QuickStart = null;
let fallbackLanguageSameCountry: QuickStart = null;
let fallbackLanguageNoCountry: QuickStart = null;
let fallbackLanguageAnyCountry: QuickStart = null;

for (const quickStart of quickStarts) {
const quickStartLanguage = (
quickStart.metadata?.labels?.[LOCALIZATION_LANGUAGE_LABEL] || 'en'
).toLowerCase();
const quickStartCountry = (
quickStart.metadata?.labels?.[LOCALIZATION_COUNTRY_LABEL] || ''
).toUpperCase();

if (quickStartLanguage === preferredLanguage && quickStartCountry === preferredCountry) {
return quickStart;
}
if (quickStartLanguage === preferredLanguage) {
if (!quickStartCountry && !sameLanguageWithoutCountry) {
sameLanguageWithoutCountry = quickStart;
} else if (quickStartCountry && !sameLanguageWithAnyCountry) {
sameLanguageWithAnyCountry = quickStart;
}
}
if (quickStartLanguage === 'en') {
if (quickStartCountry === preferredCountry && !fallbackLanguageSameCountry) {
fallbackLanguageSameCountry = quickStart;
} else if (!quickStartCountry && !fallbackLanguageNoCountry) {
fallbackLanguageNoCountry = quickStart;
} else if (!fallbackLanguageAnyCountry) {
fallbackLanguageAnyCountry = quickStart;
}
}
}
return (
sameLanguageWithoutCountry ||
sameLanguageWithAnyCountry ||
fallbackLanguageSameCountry ||
fallbackLanguageNoCountry ||
fallbackLanguageAnyCountry
);
};

export const useQuickStarts = (filterDisabledQuickStarts = true): WatchK8sResult<QuickStart[]> => {
const preferredLanguage = useTranslation().i18n.language;

const [quickStarts, quickStartsLoaded, quickStartsError] = useK8sWatchResource<QuickStart[]>({
groupVersionKind: getGroupVersionKindForModel(QuickStartModel),
isList: true,
});

const bestMatchQuickStarts = React.useMemo(() => {
if (!quickStartsLoaded) {
return [];
}
const groupedQuickStarts = groupQuickStartsByName(quickStarts);

if (filterDisabledQuickStarts) {
const disabledQuickStarts = getDisabledQuickStarts();
disabledQuickStarts.forEach((quickStartName) => delete groupedQuickStarts[quickStartName]);
}

return Object.values(groupedQuickStarts).map((quickStartsByName) =>
getBestMatch(quickStartsByName, preferredLanguage),
);
}, [quickStarts, quickStartsLoaded, filterDisabledQuickStarts, preferredLanguage]);

return [bestMatchQuickStarts, quickStartsLoaded, quickStartsError];
};
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type CatalogExtensionHookOptions = {
export type CatalogItem<T extends any = any> = {
uid: string;
type: string;
typeLabel?: string | React.ReactNode;
name: string;
/** Optional title to render a custom title using ReactNode.
* Rendered in catalog tile and side panel
Expand Down
Loading

0 comments on commit 504df7d

Please sign in to comment.