Skip to content

Commit

Permalink
Add support for perspective detection using extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitkrai03 committed Jul 9, 2020
1 parent 3f882db commit 49450e8
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 23 deletions.
14 changes: 0 additions & 14 deletions frontend/__tests__/reducers/ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@ describe('getDefaultPerspective', () => {
expect(getDefaultPerspective()).toBeUndefined();
});

it('should default to perspective extension marked default', () => {
// return Perspectives extension with one marked as the default
spyOn(pluginStore, 'getAllExtensions').and.returnValue([
{
type: 'Perspective',
properties: {
id: 'admin',
default: true,
},
} as Perspective,
]);
expect(getDefaultPerspective()).toBe('admin');
});

it('should default to localStorage if perspective is a valid extension', () => {
// return Perspectives extension whose id matches that in the localStorage
spyOn(pluginStore, 'getAllExtensions').and.returnValue([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import { connect, Dispatch } from 'react-redux';
import { getActivePerspective } from '@console/internal/reducers/ui';
import { RootState } from '@console/internal/redux';
import * as UIActions from '@console/internal/actions/ui';
import PerspectiveDetector from './PerspectiveDetector';

type OwnProps = {
children: React.ReactNode;
};

type StateProps = {
activePerspective: string;
};

type DispatchProps = {
setActivePerspective: (string) => void;
};

type DetectPerspectiveProps = OwnProps & StateProps & DispatchProps;

const DetectPerspective: React.FC<DetectPerspectiveProps> = ({
activePerspective,
children,
setActivePerspective,
}) =>
activePerspective ? (
<>{children}</>
) : (
<PerspectiveDetector setActivePerspective={setActivePerspective} />
);

const mapStateToProps = (state: RootState) => ({
activePerspective: getActivePerspective(state),
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
setActivePerspective: (perspective) => dispatch(UIActions.setActivePerspective(perspective)),
});

// For testing
export const InternalDetectPerspective = DetectPerspective;

export default connect<StateProps, DispatchProps, OwnProps>(
mapStateToProps,
mapDispatchToProps,
)(DetectPerspective);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import { useExtensions, isPerspective } from '@console/plugin-sdk';

type PerspectiveDetectorProps = {
setActivePerspective: (string) => void;
};

const PerspectiveDetector: React.FC<PerspectiveDetectorProps> = ({ setActivePerspective }) => {
let detectedPerspective: string;
const perspectiveExtensions = useExtensions(isPerspective);
const defaultPerspective = perspectiveExtensions.find((p) => p.properties.default);
const detectors = perspectiveExtensions.filter((p) => p.properties.usePerspectiveDetection);
const detectionResults = detectors.map((p) => p.properties.usePerspectiveDetection());

const detectionComplete = detectionResults.every((result, index) => {
const [enablePerspective, loading] = result;
if (!detectedPerspective && !loading && enablePerspective) {
detectedPerspective = detectors[index].properties.id;
}
return loading === false;
});

React.useEffect(() => {
if (detectedPerspective) {
setActivePerspective(detectedPerspective);
} else if (detectors.length < 1 || detectionComplete) {
setActivePerspective(defaultPerspective.properties.id); // set default perspective if there are no detectors or none of the detections were successfull
}
}, [
defaultPerspective,
detectedPerspective,
detectionComplete,
detectors.length,
setActivePerspective,
]);

return null;
};

export default PerspectiveDetector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { InternalDetectPerspective } from '../DetectPerspective';
import PerspectiveDetector from '../PerspectiveDetector';

const MockApp = () => <h1>App</h1>;

describe('DetectPerspective', () => {
it('should render children if there is an activePerspective', () => {
const wrapper = shallow(
<InternalDetectPerspective activePerspective="dev" setActivePerspective={() => {}}>
<MockApp />
</InternalDetectPerspective>,
);
expect(wrapper.find(MockApp).exists()).toBe(true);
});

it('should render PerspectiveDetector if there is no activePerspective', () => {
const wrapper = shallow(
<InternalDetectPerspective activePerspective={undefined} setActivePerspective={() => {}}>
<MockApp />
</InternalDetectPerspective>,
);
expect(wrapper.find(PerspectiveDetector).exists()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from 'react';
import { mount } from 'enzyme';
import { Perspective, useExtensions } from '@console/plugin-sdk';
import PerspectiveDetector from '../PerspectiveDetector';

jest.mock('@console/plugin-sdk', () => ({
useExtensions: jest.fn(),
}));

const mockPerspectives = [
{
type: 'Perspective',
properties: {
id: 'admin',
name: 'Admin Perspective',
default: true,
},
},
{
type: 'Perspective',
properties: {
id: 'dev',
name: 'Dev Perspective',
usePerspectiveDetection: undefined,
},
},
] as Perspective[];

const setActivePerspective = jest.fn();

describe('PerspectiveDetector', () => {
it('should set default perspective if there are no perspective detectors available', () => {
(useExtensions as jest.Mock).mockImplementation(() => mockPerspectives);

const wrapper = mount(<PerspectiveDetector setActivePerspective={setActivePerspective} />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(setActivePerspective).toHaveBeenCalledWith('admin');
});

it('should set detected perspective if detection is successful', () => {
mockPerspectives[1].properties.usePerspectiveDetection = () => [true, false];
(useExtensions as jest.Mock).mockImplementation(() => mockPerspectives);

const wrapper = mount(<PerspectiveDetector setActivePerspective={setActivePerspective} />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(setActivePerspective).toHaveBeenCalledWith('dev');
});

it('should set default perspective if detection fails', () => {
mockPerspectives[1].properties.usePerspectiveDetection = () => [false, false];
(useExtensions as jest.Mock).mockImplementation(() => mockPerspectives);

const wrapper = mount(<PerspectiveDetector setActivePerspective={setActivePerspective} />);
expect(wrapper.isEmptyRender()).toBe(true);
expect(setActivePerspective).toHaveBeenCalledWith('admin');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace ExtensionProperties {
getK8sLandingPageURL: GetLandingPage;
/** The function to get redirect URL for import flow. */
getImportRedirectURL: (project: string) => string;
/** The hook to detect default perspective */
usePerspectiveDetection?: () => [boolean, boolean]; // [enablePerspective: boolean, loading: boolean]
}
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/packages/dev-console/src/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
OperatorsTopologyConsumedExtensions,
operatorsTopologyPlugin,
} from './components/topology/operators/operatorsTopologyPlugin';
import { usePerspectiveDetection } from './utils/usePerspectiveDetection';

const {
ClusterTaskModel,
Expand Down Expand Up @@ -422,6 +423,7 @@ const plugin: Plugin<ConsumedExtensions> = [
getLandingPageURL: () => '/topology',
getK8sLandingPageURL: () => '/add',
getImportRedirectURL: (project) => `/topology/ns/${project}`,
usePerspectiveDetection,
},
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore: FIXME missing exports due to out-of-sync @types/react-redux version
import { useSelector } from 'react-redux';
import { testHook } from '@console/shared/src/test-utils/hooks-utils';
import { usePerspectiveDetection } from '../usePerspectiveDetection';

jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));

describe('usePerspectiveDetection', () => {
it('should return loading as true if CAN_GET_NS flag is pending', () => {
(useSelector as jest.Mock).mockImplementation(() => ({
CAN_GET_NS: undefined,
}));

testHook(() => {
const [enablePerspective, loading] = usePerspectiveDetection();

expect(enablePerspective).toBe(true);
expect(loading).toBe(true);
});
});

it('should return loading as false if CAN_GET_NS flag is loaded', () => {
(useSelector as jest.Mock).mockImplementation(() => ({
CAN_GET_NS: false,
}));

testHook(() => {
const [enablePerspective, loading] = usePerspectiveDetection();

expect(enablePerspective).toBe(true);
expect(loading).toBe(false);
});
});
});
14 changes: 14 additions & 0 deletions frontend/packages/dev-console/src/utils/usePerspectiveDetection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore: FIXME missing exports due to out-of-sync @types/react-redux version
import { useSelector } from 'react-redux';
import { RootState } from '@console/internal/redux';
import { getFlagsObject, flagPending } from '@console/internal/reducers/features';

export const usePerspectiveDetection = () => {
const flags = useSelector((state: RootState) => getFlagsObject(state));
const canGetNS = flags.CAN_GET_NS;
const loadingFlag = flagPending(canGetNS);
const enablePerspective = !canGetNS;

return [enablePerspective, loadingFlag] as [boolean, boolean];
};
6 changes: 4 additions & 2 deletions frontend/public/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { receivedResources, watchAPIServices } from '../actions/k8s';
// cloud shell imports must come later than features
import CloudShell from '@console/app/src/components/cloud-shell/CloudShell';
import CloudShellTab from '@console/app/src/components/cloud-shell/CloudShellTab';
import DetectPerspective from '@console/app/src/components/detect-perspective/DetectPerspective';

const consoleLoader = () =>
import(
'@console/kubevirt-plugin/src/components/connected-vm-console/vm-console-page' /* webpackChunkName: "kubevirt" */
Expand Down Expand Up @@ -130,7 +132,7 @@ class App extends React.PureComponent {
const { productName } = getBrandingDetails();

return (
<>
<DetectPerspective>
<Helmet titleTemplate={`%s · ${productName}`} defaultTitle={productName} />
<ConsoleNotifier location="BannerTop" />
<Page
Expand All @@ -152,7 +154,7 @@ class App extends React.PureComponent {
</Page>
<CloudShell />
<ConsoleNotifier location="BannerBottom" />
</>
</DetectPerspective>
);
}
}
Expand Down
7 changes: 0 additions & 7 deletions frontend/public/reducers/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ export function getDefaultPerspective() {
// invalid saved perspective
activePerspective = undefined;
}
if (!activePerspective) {
// assign default perspective
const defaultPerspective = perspectiveExtensions.find((p) => p.properties.default);
if (defaultPerspective) {
activePerspective = defaultPerspective.properties.id;
}
}
return activePerspective || undefined;
}

Expand Down

0 comments on commit 49450e8

Please sign in to comment.