mirror of https://github.com/portainer/portainer
refactor(azure/aci): migrate dashboard view to react [EE-2189] (#6518)
* refactor(azure/aci): migrate dashboard view to react [EE-2189] * move aggregate function to azure utils file * fix type * introduce dashboard item component * add error notificatons * hide resource groups widget if failed to load * make dashboard a default export * revert mistake * refactor based on suggestions * use object for error data instead of array * return unused utils file * move length calculations out of return statement * only return first error of resource groups queries * refactor imports/exports, fix bug with errors & add test * WIP dashboard tests * allow mocking multiple resource groups * test for total number of resource groups * update lock file to fix lint action issue * finish dashboard tests * dashboarditem story * fix(auth): remove caching of user * add option for link to dashboard item * rename dashboard test case to match file * remove optional link and update storybook * create aria label based on already provided text * change param name to be clearerpull/6595/head^2
parent
ff7847aaa5
commit
a894e3182a
|
@ -0,0 +1,144 @@
|
||||||
|
import { renderWithQueryClient, within } from '@/react-tools/test-utils';
|
||||||
|
import { UserContext } from '@/portainer/hooks/useUser';
|
||||||
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
|
import { server, rest } from '@/setup-tests/server';
|
||||||
|
import {
|
||||||
|
createMockResourceGroups,
|
||||||
|
createMockSubscriptions,
|
||||||
|
} from '@/react-tools/test-mocks';
|
||||||
|
|
||||||
|
import { DashboardView } from './DashboardView';
|
||||||
|
|
||||||
|
jest.mock('@uirouter/react', () => ({
|
||||||
|
...jest.requireActual('@uirouter/react'),
|
||||||
|
useCurrentStateAndParams: jest.fn(() => ({
|
||||||
|
params: { endpointId: 1 },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('dashboard items should render correctly', async () => {
|
||||||
|
const { getByLabelText } = await renderComponent();
|
||||||
|
|
||||||
|
const subscriptionsItem = getByLabelText('Subscriptions');
|
||||||
|
expect(subscriptionsItem).toBeVisible();
|
||||||
|
|
||||||
|
const subscriptionElements = within(subscriptionsItem);
|
||||||
|
expect(subscriptionElements.getByLabelText('value')).toBeVisible();
|
||||||
|
expect(subscriptionElements.getByLabelText('icon')).toHaveClass('fa-th-list');
|
||||||
|
expect(subscriptionElements.getByLabelText('resourceType')).toHaveTextContent(
|
||||||
|
'Subscriptions'
|
||||||
|
);
|
||||||
|
|
||||||
|
const resourceGroupsItem = getByLabelText('Resource groups');
|
||||||
|
expect(resourceGroupsItem).toBeVisible();
|
||||||
|
|
||||||
|
const resourceGroupElements = within(resourceGroupsItem);
|
||||||
|
expect(resourceGroupElements.getByLabelText('value')).toBeVisible();
|
||||||
|
expect(resourceGroupElements.getByLabelText('icon')).toHaveClass(
|
||||||
|
'fa-th-list'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
resourceGroupElements.getByLabelText('resourceType')
|
||||||
|
).toHaveTextContent('Resource groups');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when there are no subscriptions, should show 0 subscriptions and 0 resource groups', async () => {
|
||||||
|
const { getByLabelText } = await renderComponent();
|
||||||
|
|
||||||
|
const subscriptionElements = within(getByLabelText('Subscriptions'));
|
||||||
|
expect(subscriptionElements.getByLabelText('value')).toHaveTextContent('0');
|
||||||
|
|
||||||
|
const resourceGroupElements = within(getByLabelText('Resource groups'));
|
||||||
|
expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when there is subscription & resource group data, should display these', async () => {
|
||||||
|
const { getByLabelText } = await renderComponent(1, { 'subscription-1': 2 });
|
||||||
|
|
||||||
|
const subscriptionElements = within(getByLabelText('Subscriptions'));
|
||||||
|
expect(subscriptionElements.getByLabelText('value')).toHaveTextContent('1');
|
||||||
|
|
||||||
|
const resourceGroupElements = within(getByLabelText('Resource groups'));
|
||||||
|
expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should correctly show total number of resource groups across multiple subscriptions', async () => {
|
||||||
|
const { getByLabelText } = await renderComponent(2, {
|
||||||
|
'subscription-1': 2,
|
||||||
|
'subscription-2': 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resourceGroupElements = within(getByLabelText('Resource groups'));
|
||||||
|
expect(resourceGroupElements.getByLabelText('value')).toHaveTextContent('5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when only subscriptions fail to load, dont show the dashboard', async () => {
|
||||||
|
const { queryByLabelText } = await renderComponent(
|
||||||
|
1,
|
||||||
|
{ 'subscription-1': 1 },
|
||||||
|
500,
|
||||||
|
200
|
||||||
|
);
|
||||||
|
expect(queryByLabelText('Subscriptions')).not.toBeInTheDocument();
|
||||||
|
expect(queryByLabelText('Resource groups')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when only resource groups fail to load, still show the subscriptions', async () => {
|
||||||
|
const { queryByLabelText } = await renderComponent(
|
||||||
|
1,
|
||||||
|
{ 'subscription-1': 1 },
|
||||||
|
200,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
expect(queryByLabelText('Subscriptions')).toBeInTheDocument();
|
||||||
|
expect(queryByLabelText('Resource groups')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function renderComponent(
|
||||||
|
subscriptionsCount = 0,
|
||||||
|
resourceGroups: Record<string, number> = {},
|
||||||
|
subscriptionsStatus = 200,
|
||||||
|
resourceGroupsStatus = 200
|
||||||
|
) {
|
||||||
|
const user = new UserViewModel({ Username: 'user' });
|
||||||
|
const state = { user };
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(
|
||||||
|
'/api/endpoints/:endpointId/azure/subscriptions',
|
||||||
|
(req, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.json(createMockSubscriptions(subscriptionsCount)),
|
||||||
|
ctx.status(subscriptionsStatus)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
rest.get(
|
||||||
|
'/api/endpoints/:endpointId/azure/subscriptions/:subscriptionId/resourcegroups',
|
||||||
|
(req, res, ctx) => {
|
||||||
|
if (typeof req.params.subscriptionId !== 'string') {
|
||||||
|
throw new Error("Provided subscriptionId must be of type: 'string'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { subscriptionId } = req.params;
|
||||||
|
return res(
|
||||||
|
ctx.json(
|
||||||
|
createMockResourceGroups(
|
||||||
|
req.params.subscriptionId,
|
||||||
|
resourceGroups[subscriptionId] || 0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ctx.status(resourceGroupsStatus)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const renderResult = renderWithQueryClient(
|
||||||
|
<UserContext.Provider value={state}>
|
||||||
|
<DashboardView />
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(renderResult.findByText(/Home/)).resolves.toBeVisible();
|
||||||
|
|
||||||
|
return renderResult;
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
|
||||||
|
import { PageHeader } from '@/portainer/components/PageHeader';
|
||||||
|
import { DashboardItem } from '@/portainer/components/Dashboard/DashboardItem';
|
||||||
|
import { error as notifyError } from '@/portainer/services/notifications';
|
||||||
|
import PortainerError from '@/portainer/error';
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import { useResourceGroups, useSubscriptions } from '../queries';
|
||||||
|
|
||||||
|
export function DashboardView() {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
|
||||||
|
const subscriptionsQuery = useSubscriptions(environmentId);
|
||||||
|
useEffect(() => {
|
||||||
|
if (subscriptionsQuery.isError) {
|
||||||
|
notifyError(
|
||||||
|
'Failure',
|
||||||
|
subscriptionsQuery.error as PortainerError,
|
||||||
|
'Unable to retrieve subscriptions'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [subscriptionsQuery.error, subscriptionsQuery.isError]);
|
||||||
|
|
||||||
|
const resourceGroupsQuery = useResourceGroups(
|
||||||
|
environmentId,
|
||||||
|
subscriptionsQuery.data
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (resourceGroupsQuery.isError && resourceGroupsQuery.error) {
|
||||||
|
notifyError(
|
||||||
|
'Failure',
|
||||||
|
resourceGroupsQuery.error as PortainerError,
|
||||||
|
`Unable to retrieve resource groups`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [resourceGroupsQuery.error, resourceGroupsQuery.isError]);
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
subscriptionsQuery.isLoading || resourceGroupsQuery.isLoading;
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionsCount = subscriptionsQuery?.data?.length;
|
||||||
|
const resourceGroupsCount = Object.values(
|
||||||
|
resourceGroupsQuery?.resourceGroups
|
||||||
|
).flatMap((x) => Object.values(x)).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Home" breadcrumbs={[{ label: 'Dashboard' }]} />
|
||||||
|
|
||||||
|
{!subscriptionsQuery.isError && (
|
||||||
|
<div className="row">
|
||||||
|
<DashboardItem
|
||||||
|
value={subscriptionsCount as number}
|
||||||
|
icon="fa fa-th-list"
|
||||||
|
type="Subscriptions"
|
||||||
|
/>
|
||||||
|
{!resourceGroupsQuery.isError && (
|
||||||
|
<DashboardItem
|
||||||
|
value={resourceGroupsCount as number}
|
||||||
|
icon="fa fa-th-list"
|
||||||
|
type="Resource groups"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardViewAngular = r2a(DashboardView, []);
|
|
@ -0,0 +1 @@
|
||||||
|
export { DashboardViewAngular, DashboardView } from './DashboardView';
|
|
@ -1,82 +1,85 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { DashboardViewAngular } from './Dashboard/DashboardView';
|
||||||
import { containerInstancesModule } from './ContainerInstances';
|
import { containerInstancesModule } from './ContainerInstances';
|
||||||
|
|
||||||
angular.module('portainer.azure', ['portainer.app', containerInstancesModule]).config([
|
angular
|
||||||
'$stateRegistryProvider',
|
.module('portainer.azure', ['portainer.app', containerInstancesModule])
|
||||||
function ($stateRegistryProvider) {
|
.config([
|
||||||
'use strict';
|
'$stateRegistryProvider',
|
||||||
|
function ($stateRegistryProvider) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
var azure = {
|
var azure = {
|
||||||
name: 'azure',
|
name: 'azure',
|
||||||
url: '/azure',
|
url: '/azure',
|
||||||
parent: 'endpoint',
|
parent: 'endpoint',
|
||||||
abstract: true,
|
abstract: true,
|
||||||
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, Notifications, StateManager) {
|
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, Notifications, StateManager) {
|
||||||
return $async(async () => {
|
return $async(async () => {
|
||||||
if (endpoint.Type !== 3) {
|
if (endpoint.Type !== 3) {
|
||||||
$state.go('portainer.home');
|
$state.go('portainer.home');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||||
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
|
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
|
||||||
await StateManager.updateEndpointState(endpoint);
|
await StateManager.updateEndpointState(endpoint, []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Notifications.error('Failed loading environment', e);
|
Notifications.error('Failed loading environment', e);
|
||||||
$state.go('portainer.home', {}, { reload: true });
|
$state.go('portainer.home', {}, { reload: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var containerInstances = {
|
|
||||||
name: 'azure.containerinstances',
|
|
||||||
url: '/containerinstances',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containerinstances/containerinstances.html',
|
|
||||||
controller: 'AzureContainerInstancesController',
|
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
var containerInstance = {
|
var containerInstances = {
|
||||||
name: 'azure.containerinstances.container',
|
name: 'azure.containerinstances',
|
||||||
url: '/:id',
|
url: '/containerinstances',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'containerInstanceDetails',
|
templateUrl: './views/containerinstances/containerinstances.html',
|
||||||
|
controller: 'AzureContainerInstancesController',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
var containerInstanceCreation = {
|
var containerInstance = {
|
||||||
name: 'azure.containerinstances.new',
|
name: 'azure.containerinstances.container',
|
||||||
url: '/new/',
|
url: '/:id',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'createContainerInstanceView',
|
component: 'containerInstanceDetails',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
var dashboard = {
|
var containerInstanceCreation = {
|
||||||
name: 'azure.dashboard',
|
name: 'azure.containerinstances.new',
|
||||||
url: '/dashboard',
|
url: '/new/',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: './views/dashboard/dashboard.html',
|
component: 'createContainerInstanceView',
|
||||||
controller: 'AzureDashboardController',
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
|
|
||||||
$stateRegistryProvider.register(azure);
|
var dashboard = {
|
||||||
$stateRegistryProvider.register(containerInstances);
|
name: 'azure.dashboard',
|
||||||
$stateRegistryProvider.register(containerInstance);
|
url: '/dashboard',
|
||||||
$stateRegistryProvider.register(containerInstanceCreation);
|
views: {
|
||||||
$stateRegistryProvider.register(dashboard);
|
'content@': {
|
||||||
},
|
component: 'dashboardView',
|
||||||
]);
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
$stateRegistryProvider.register(azure);
|
||||||
|
$stateRegistryProvider.register(containerInstances);
|
||||||
|
$stateRegistryProvider.register(containerInstance);
|
||||||
|
$stateRegistryProvider.register(containerInstanceCreation);
|
||||||
|
$stateRegistryProvider.register(dashboard);
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.component('dashboardView', DashboardViewAngular).name;
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useQueries, useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { getResourceGroups } from './services/resource-groups.service';
|
||||||
|
import { getSubscriptions } from './services/subscription.service';
|
||||||
|
import { Subscription } from './types';
|
||||||
|
|
||||||
|
export function useSubscriptions(environmentId: EnvironmentId) {
|
||||||
|
return useQuery(
|
||||||
|
'azure.subscriptions',
|
||||||
|
() => getSubscriptions(environmentId),
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
error: {
|
||||||
|
title: 'Failure',
|
||||||
|
message: 'Unable to retrieve Azure subscriptions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResourceGroups(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
subscriptions: Subscription[] = []
|
||||||
|
) {
|
||||||
|
const queries = useQueries(
|
||||||
|
subscriptions.map((subscription) => ({
|
||||||
|
queryKey: [
|
||||||
|
'azure',
|
||||||
|
environmentId,
|
||||||
|
'subscriptions',
|
||||||
|
subscription.subscriptionId,
|
||||||
|
'resourceGroups',
|
||||||
|
],
|
||||||
|
queryFn: async () => {
|
||||||
|
const groups = await getResourceGroups(
|
||||||
|
environmentId,
|
||||||
|
subscription.subscriptionId
|
||||||
|
);
|
||||||
|
return [subscription.subscriptionId, groups] as const;
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
error: {
|
||||||
|
title: 'Failure',
|
||||||
|
message: 'Unable to retrieve Azure resource groups',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
resourceGroups: Object.fromEntries(
|
||||||
|
_.compact(
|
||||||
|
queries.map((q) => {
|
||||||
|
if (q.data) {
|
||||||
|
return q.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
isLoading: queries.some((q) => q.isLoading),
|
||||||
|
isError: queries.some((q) => q.isError),
|
||||||
|
error: queries.find((q) => q.error)?.error || null,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,33 +0,0 @@
|
||||||
<rd-header>
|
|
||||||
<rd-header-title title-text="Home"></rd-header-title>
|
|
||||||
<rd-header-content>Dashboard</rd-header-content>
|
|
||||||
</rd-header>
|
|
||||||
|
|
||||||
<div class="row" ng-if="subscriptions">
|
|
||||||
<div class="col-sm-12 col-md-6">
|
|
||||||
<a ui-sref="azure.subscriptions">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon blue pull-left">
|
|
||||||
<i class="fa fa-th-list"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ subscriptions.length }}</div>
|
|
||||||
<div class="comment">Subscriptions</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 col-md-6" ng-if="resourceGroups">
|
|
||||||
<a ui-sref="azure.resourceGroups">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon blue pull-left">
|
|
||||||
<i class="fa fa-th-list"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ resourceGroups.length }}</div>
|
|
||||||
<div class="comment">Resource groups</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,23 +0,0 @@
|
||||||
angular.module('portainer.azure').controller('AzureDashboardController', [
|
|
||||||
'$scope',
|
|
||||||
'AzureService',
|
|
||||||
'Notifications',
|
|
||||||
function ($scope, AzureService, Notifications) {
|
|
||||||
function initView() {
|
|
||||||
AzureService.subscriptions()
|
|
||||||
.then(function success(data) {
|
|
||||||
var subscriptions = data;
|
|
||||||
$scope.subscriptions = subscriptions;
|
|
||||||
return AzureService.resourceGroups(subscriptions);
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
$scope.resourceGroups = AzureService.aggregate(data);
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to load dashboard data');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initView();
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Link } from '@/portainer/components/Link';
|
||||||
|
|
||||||
|
import { DashboardItem } from './DashboardItem';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/DashboardItem',
|
||||||
|
component: DashboardItem,
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
interface StoryProps {
|
||||||
|
value: number;
|
||||||
|
icon: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Template({ value, icon, type }: StoryProps) {
|
||||||
|
return <DashboardItem value={value} icon={icon} type={type} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Primary: Story<StoryProps> = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
value: 1,
|
||||||
|
icon: 'fa fa-th-list',
|
||||||
|
type: 'Example resource',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WithLink() {
|
||||||
|
return (
|
||||||
|
<Link to="example.page">
|
||||||
|
<DashboardItem value={1} icon="fa fa-th-list" type="Example resource" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { render } from '@/react-tools/test-utils';
|
||||||
|
|
||||||
|
import { DashboardItem } from './DashboardItem';
|
||||||
|
|
||||||
|
test('should show provided resource value', async () => {
|
||||||
|
const { getByLabelText } = renderComponent(1);
|
||||||
|
const value = getByLabelText('value');
|
||||||
|
|
||||||
|
expect(value).toBeVisible();
|
||||||
|
expect(value).toHaveTextContent('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show provided icon', async () => {
|
||||||
|
const { getByLabelText } = renderComponent(0, 'fa fa-th-list');
|
||||||
|
const icon = getByLabelText('icon');
|
||||||
|
expect(icon).toHaveClass('fa-th-list');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show provided resource type', async () => {
|
||||||
|
const { getByLabelText } = renderComponent(0, '', 'Test');
|
||||||
|
const title = getByLabelText('resourceType');
|
||||||
|
|
||||||
|
expect(title).toBeVisible();
|
||||||
|
expect(title).toHaveTextContent('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have accessibility label created from the provided resource type', async () => {
|
||||||
|
const { getByLabelText } = renderComponent(0, '', 'testLabel');
|
||||||
|
|
||||||
|
expect(getByLabelText('testLabel')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderComponent(value = 0, icon = '', type = '') {
|
||||||
|
return render(<DashboardItem value={value} icon={icon} type={type} />);
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Widget, WidgetBody } from '@/portainer/components/widget';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: number;
|
||||||
|
icon: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardItem({ value, icon, type }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="col-sm-12 col-md-6" aria-label={type}>
|
||||||
|
<Widget>
|
||||||
|
<WidgetBody>
|
||||||
|
<div className="widget-icon blue pull-left">
|
||||||
|
<i className={icon} aria-hidden="true" aria-label="icon" />
|
||||||
|
</div>
|
||||||
|
<div className="title" aria-label="value">
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
<div className="comment" aria-label="resourceType">
|
||||||
|
{type}
|
||||||
|
</div>
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
export function useEnvironmentId() {
|
||||||
|
const {
|
||||||
|
params: { endpointId: environmentId },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
|
if (!environmentId) {
|
||||||
|
throw new Error('endpointId url param is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return environmentId;
|
||||||
|
}
|
|
@ -19,8 +19,6 @@ interface State {
|
||||||
user?: UserViewModel | null;
|
user?: UserViewModel | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = {};
|
|
||||||
|
|
||||||
export const UserContext = createContext<State | null>(null);
|
export const UserContext = createContext<State | null>(null);
|
||||||
|
|
||||||
export function useUser() {
|
export function useUser() {
|
||||||
|
@ -93,9 +91,7 @@ export function UserProvider({ children }: UserProviderProps) {
|
||||||
const [user, setUser] = useState<UserViewModel | null>(null);
|
const [user, setUser] = useState<UserViewModel | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.user) {
|
if (jwt !== '') {
|
||||||
setUser(state.user);
|
|
||||||
} else if (jwt !== '') {
|
|
||||||
const tokenPayload = jwtDecode(jwt) as { id: number };
|
const tokenPayload = jwtDecode(jwt) as { id: number };
|
||||||
|
|
||||||
loadUser(tokenPayload.id);
|
loadUser(tokenPayload.id);
|
||||||
|
@ -120,7 +116,6 @@ export function UserProvider({ children }: UserProviderProps) {
|
||||||
|
|
||||||
async function loadUser(id: number) {
|
async function loadUser(id: number) {
|
||||||
const user = await getUser(id);
|
const user = await getUser(id);
|
||||||
state.user = user;
|
|
||||||
setUser(user);
|
setUser(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,3 +20,21 @@ export function createMockTeams(count: number) {
|
||||||
Name: `team${value}`,
|
Name: `team${value}`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMockSubscriptions(count: number) {
|
||||||
|
const subscriptions = _.range(1, count + 1).map((x) => ({
|
||||||
|
id: `/subscriptions/subscription-${x}`,
|
||||||
|
subscriptionId: `subscription-${x}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { value: subscriptions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMockResourceGroups(subscription: string, count: number) {
|
||||||
|
const resourceGroups = _.range(1, count + 1).map((x) => ({
|
||||||
|
id: `/subscriptions/${subscription}/resourceGroups/resourceGroup-${x}`,
|
||||||
|
name: `resourcegroup-${x}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { value: resourceGroups };
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue