Skip to content

Commit

Permalink
adds support for export app in topology
Browse files Browse the repository at this point in the history
  • Loading branch information
invincibleJai committed Aug 20, 2021
1 parent 8c3bf59 commit 10234fa
Show file tree
Hide file tree
Showing 14 changed files with 475 additions and 6 deletions.
21 changes: 21 additions & 0 deletions frontend/packages/topology/console-extensions.json
@@ -1,4 +1,15 @@
[
{
"type": "console.flag/model",
"properties": {
"model": {
"group": "primer.gitops.io",
"version": "v1alpha1",
"kind": "Export"
},
"flag": "ALLOW_EXPORT_APP"
}
},
{
"type": "console.redux-reducer",
"properties": {
Expand Down Expand Up @@ -162,5 +173,15 @@
"$codeRef": "workload.useHealthChecksAlert"
}
}
},
{
"type": "console.context-provider",
"properties": {
"provider": { "$codeRef": "exportAppContext.ExportAppContextProvider" },
"useValueHook": { "$codeRef": "exportAppContext.useExportAppFormToast" }
},
"flags": {
"required": ["ALLOW_EXPORT_APP"]
}
}
]
8 changes: 8 additions & 0 deletions frontend/packages/topology/locales/en/topology.json
Expand Up @@ -17,6 +17,14 @@
"Application name": "Application name",
"all applications": "all applications",
"no application group": "no application group",
"Export Application": "Export Application",
"All the resources are exported successfully from {{namespace}}. Click below to download it.": "All the resources are exported successfully from {{namespace}}. Click below to download it.",
"Download": "Download",
"Application export in <1>{{namespace}}</1> is in progress. Started at {{startTime}}.": "Application export in <1>{{namespace}}</1> is in progress. Started at {{startTime}}.",
"Application export in <1>{{namespace}}</1> is in progress.": "Application export in <1>{{namespace}}</1> is in progress.",
"OK": "OK",
"Export of resources in <1>{{namespace}}</1> has started.": "Export of resources in <1>{{namespace}}</1> has started.",
"Export of resources in <1>{{namespace}}</1> has failed with error: {error.message}": "Export of resources in <1>{{namespace}}</1> has failed with error: {error.message}",
"Error moving connection": "Error moving connection",
"Error creating connection": "Error creating connection",
"Add resources": "Add resources",
Expand Down
3 changes: 2 additions & 1 deletion frontend/packages/topology/package.json
Expand Up @@ -25,7 +25,8 @@
"exposedModules": {
"reduxReducer": "src/utils/reducer.ts",
"workload": "src/components/workload",
"actions": "src/actions/provider.ts"
"actions": "src/actions/provider.ts",
"exportAppContext": "src/components/export-app/export-app-context.ts"
}
}
}
@@ -0,0 +1,126 @@
import * as React from 'react';
import { ToolbarItem, Button, AlertVariant } from '@patternfly/react-core';
import * as _ from 'lodash';
import { useTranslation, Trans } from 'react-i18next';
import { useAccessReview } from '@console/internal/components/utils';
import { dateTimeFormatter } from '@console/internal/components/utils/datetime';
import { k8sCreate, k8sGet, k8sKill, K8sResourceKind } from '@console/internal/module/k8s';
import {
useFlag,
useIsMobile,
USERSETTINGS_PREFIX,
useToast,
useUserSettings,
} from '@console/shared/src';
import { ALLOW_EXPORT_APP, EXPORT_CR_NAME } from '../../const';
import { ExportModel } from '../../models';
import { getExportAppData } from '../../utils/export-app-utils';
import exportApplicationModal from './export-app-modal';
import { ExportAppUserSettings } from './types';

type ExportApplicationProps = {
namespace: string;
isDisabled: boolean;
};

const ExportApplication: React.FC<ExportApplicationProps> = ({ namespace, isDisabled }) => {
const { t } = useTranslation();
const [isCreating, setIsCreating] = React.useState<boolean>(false);
const isMobile = useIsMobile();
const isExportAppAllowed = useFlag(ALLOW_EXPORT_APP);
const canExportApp = useAccessReview({
group: ExportModel.apiGroup,
resource: ExportModel.plural,
verb: 'create',
namespace,
});
const toast = useToast();
const [exportAppToast, setExportAppToast] = useUserSettings<ExportAppUserSettings>(
`${USERSETTINGS_PREFIX}.exportApp`,
{},
true,
);

const createExportCR = async () => {
try {
const exportResp = await k8sCreate<K8sResourceKind>(ExportModel, getExportAppData(namespace));
const key = `${namespace}-${exportResp.metadata.name}`;
const exportAppToastConfig = {
...exportAppToast,
[key]: {
uid: exportResp.metadata.uid,
name: exportResp.metadata.name,
kind: exportResp.kind,
namespace,
},
};
toast.addToast({
variant: AlertVariant.info,
title: t('topology~Export Application'),
content: (
<Trans t={t} ns="topology">
Export of resources in <strong>{{ namespace }}</strong> has started.
</Trans>
),
dismissible: true,
timeout: true,
});
setExportAppToast(exportAppToastConfig);
setIsCreating(false);
} catch (error) {
setIsCreating(false);
toast.addToast({
variant: AlertVariant.danger,
title: t('topology~Export Application'),
content: (
<Trans t={t} ns="topology">
Export of resources in <strong>{{ namespace }}</strong> has failed with error:{' '}
{error.message}
</Trans>
),
dismissible: true,
timeout: true,
});
}
};

const exportAppClickHandle = async () => {
try {
setIsCreating(true);
const exportRes = await k8sGet(ExportModel, EXPORT_CR_NAME, namespace);
if (exportRes && exportRes.status?.completed !== true) {
const startTime = dateTimeFormatter.format(new Date(exportRes.metadata.creationTimestamp));
exportApplicationModal({ namespace, startTime });
} else if (exportRes && exportRes.status?.completed) {
await k8sKill(ExportModel, exportRes);
const exportAppToastConfig = _.omit(exportAppToast, `${namespace}-${EXPORT_CR_NAME}`);
setExportAppToast(exportAppToastConfig);
createExportCR();
}
} catch {
if (isCreating) {
exportApplicationModal({ namespace });
return;
}
createExportCR();
}
};

const showExportAppBtn = canExportApp && isExportAppAllowed && !isMobile;

return showExportAppBtn ? (
<ToolbarItem>
<Button
variant="secondary"
data-test="export-app-btn"
aria-label={t('topology~Export Application')}
isDisabled={isDisabled}
onClick={exportAppClickHandle}
>
{t('topology~Export Application')}
</Button>
</ToolbarItem>
) : null;
};

export default ExportApplication;
@@ -0,0 +1,75 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import * as utils from '@console/internal/components/utils';
import * as k8s from '@console/internal/module/k8s';
import * as shared from '@console/shared';
import { EXPORT_CR_NAME } from '../../../const';
import { ExportModel } from '../../../models';
import ExportApplication from '../ExportApplication';
import { mockExportData } from './export-data';

jest.mock('react-i18next', () => {
const reactI18next = require.requireActual('react-i18next');
return {
...reactI18next,
useTranslation: () => ({ t: (key) => key }),
};
});

describe('ExportApplication', () => {
const spyUseAccessReview = jest.spyOn(utils, 'useAccessReview');
const spyUseFlag = jest.spyOn(shared, 'useFlag');
const spyUseIsMobile = jest.spyOn(shared, 'useIsMobile');
const spyUseToast = jest.spyOn(shared, 'useToast');
const spyUseUserSettings = jest.spyOn(shared, 'useUserSettings');

beforeEach(() => {
spyUseToast.mockReturnValue({ addToast: (v) => ({ v }) });
spyUseUserSettings.mockReturnValue([{}, jest.fn(), false]);
});

afterEach(() => {
jest.resetAllMocks();
});

it('should render export app btn when feature flag is present and user has access export CR and not mobile', () => {
spyUseFlag.mockReturnValue(true);
spyUseAccessReview.mockReturnValue(true);
spyUseIsMobile.mockReturnValue(false);

const wrapper = shallow(<ExportApplication namespace="my-app" isDisabled={false} />);
expect(wrapper.find('[data-test="export-app-btn"]').exists()).toBe(true);
});

it('should not render export app btn when feature flag is present but user do not has access to create export CR and not mobile', () => {
spyUseFlag.mockReturnValue(true);
spyUseAccessReview.mockReturnValue(false);
spyUseIsMobile.mockReturnValue(false);

const wrapper = shallow(<ExportApplication namespace="my-app" isDisabled={false} />);
expect(wrapper.find('[data-test="export-app-btn"]').exists()).toBe(false);
});

it('should not render export app btn when feature flag is present and user has access to create export CR but on mobile', () => {
spyUseFlag.mockReturnValue(true);
spyUseAccessReview.mockReturnValue(true);
spyUseIsMobile.mockReturnValue(true);

const wrapper = shallow(<ExportApplication namespace="my-app" isDisabled={false} />);
expect(wrapper.find('[data-test="export-app-btn"]').exists()).toBe(false);
});

it('should call k8sGet with correct data on click of exportAppBtn', () => {
const spyk8sGet = jest.spyOn(k8s, 'k8sGet');
spyk8sGet.mockReturnValueOnce(Promise.resolve(mockExportData));
spyUseFlag.mockReturnValue(true);
spyUseAccessReview.mockReturnValue(true);
spyUseIsMobile.mockReturnValue(false);

const wrapper = shallow(<ExportApplication namespace="my-app" isDisabled={false} />);
expect(wrapper.find('[data-test="export-app-btn"]').exists()).toBe(true);
wrapper.find('[data-test="export-app-btn"]').simulate('click');
expect(spyk8sGet).toHaveBeenCalledTimes(1);
expect(spyk8sGet).toHaveBeenCalledWith(ExportModel, EXPORT_CR_NAME, 'my-app');
});
});
@@ -0,0 +1,52 @@
import { K8sResourceKind } from '@console/internal/module/k8s';

export const mockExportData: K8sResourceKind = {
apiVersion: 'primer.gitops.io/v1alpha1',
kind: 'Export',
metadata: {
creationTimestamp: '2021-08-19T08:12:36Z',
generation: 1,
managedFields: [
{
apiVersion: 'primer.gitops.io/v1alpha1',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:spec': {
'.': {},
'f:method': {},
},
},
manager: 'Mozilla',
operation: 'Update',
time: '2021-08-19T08:12:36Z',
},
{
apiVersion: 'primer.gitops.io/v1alpha1',
fieldsType: 'FieldsV1',
fieldsV1: {
'f:status': {
'.': {},
'f:completed': {},
'f:route': {},
},
},
manager: 'manager',
operation: 'Update',
subresource: 'status',
time: '2021-08-19T08:14:14Z',
},
],
name: 'primer',
namespace: 'jai-test',
resourceVersion: '132256',
uid: 'ec61b5d6-5904-4491-b02c-96873e6cdfdf',
},
spec: {
method: 'download',
},
status: {
completed: true,
route:
'https://primer-export-primer-jai-test.apps.jakumar-2021-08-18-144658.devcluster.openshift.com/ec61b5d6-5904-4491-b02c-96873e6cdfdf.zip',
},
};

0 comments on commit 10234fa

Please sign in to comment.