Skip to content

Commit

Permalink
Show ConsoleSamples resources in the samples catalog
Browse files Browse the repository at this point in the history
  • Loading branch information
jerolimov committed Jul 17, 2023
1 parent 8273091 commit 0b1bdc1
Show file tree
Hide file tree
Showing 42 changed files with 1,357 additions and 184 deletions.
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
@@ -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
@@ -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'),
];
@@ -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]);
});
});
@@ -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 that the no quick starts language and country is defined.
*
* 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];
};
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

0 comments on commit 0b1bdc1

Please sign in to comment.