Skip to content

Commit

Permalink
refactor(wizard): break up wizard endpoints screen [EE-3167] (#1629)
Browse files Browse the repository at this point in the history
  • Loading branch information
chiptus committed May 19, 2022
1 parent af767eb commit 7132c7f
Show file tree
Hide file tree
Showing 16 changed files with 406 additions and 78 deletions.
14 changes: 14 additions & 0 deletions app/angulartics.matomo/analytics-services.ts
@@ -1,5 +1,7 @@
import _ from 'lodash';

import { useSettings } from '@/portainer/settings/settings.service';

const categories = [
'docker',
'kubernetes',
Expand Down Expand Up @@ -61,6 +63,18 @@ export function push(
}
}

export function useAnalytics() {
const telemetryQuery = useSettings((settings) => settings.EnableTelemetry);

return { trackEvent: handleTrackEvent };

function handleTrackEvent(...args: Parameters<typeof trackEvent>) {
if (telemetryQuery.data) {
trackEvent(...args);
}
}
}

export function trackEvent(action: string, properties: TrackEventProps) {
/**
* @description Logs an event with an event category (Videos, Music, Games...), an event
Expand Down
22 changes: 0 additions & 22 deletions app/portainer/__module.js
Expand Up @@ -346,26 +346,6 @@ angular
},
};

const wizard = {
name: 'portainer.wizard',
url: '/wizard',
views: {
'content@': {
component: 'wizardView',
},
},
};

const wizardEndpoints = {
name: 'portainer.wizard.endpoints',
url: '/endpoints',
views: {
'content@': {
component: 'wizardEndpoints',
},
},
};

var initEndpoint = {
name: 'portainer.init.endpoint',
url: '/endpoint',
Expand Down Expand Up @@ -531,8 +511,6 @@ angular
$stateRegistryProvider.register(groupCreation);
$stateRegistryProvider.register(home);
$stateRegistryProvider.register(init);
$stateRegistryProvider.register(wizard);
$stateRegistryProvider.register(wizardEndpoints);
$stateRegistryProvider.register(initEndpoint);
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(registries);
Expand Down
@@ -0,0 +1,30 @@
import { Meta, Story } from '@storybook/react';

import { FormSection } from './FormSection';

export default {
component: FormSection,
title: 'Components/Form/FormSection',
} as Meta;

interface Args {
title: string;
content: string;
}

function Template({ title, content }: Args) {
return <FormSection title={title}>{content}</FormSection>;
}

export const Example: Story<Args> = Template.bind({});
Example.args = {
title: 'title',
content: `Content
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam egestas turpis magna,
vel pretium dui rhoncus nec. Maecenas felis purus, consectetur non porta sit amet,
auctor sed sapien. Aliquam eu nunc felis. Pellentesque pulvinar velit id quam pellentesque,
nec imperdiet dui finibus. In blandit augue nibh, nec tincidunt nisi porttitor quis.
Nullam nec nibh maximus, consequat quam sed, dapibus purus. Donec facilisis commodo mi, in commodo augue molestie sed.
`,
};
@@ -0,0 +1,17 @@
import { PropsWithChildren } from 'react';

import { FormSectionTitle } from '../FormSectionTitle';

interface Props {
title: string;
}

export function FormSection({ title, children }: PropsWithChildren<Props>) {
return (
<>
<FormSectionTitle>{title}</FormSectionTitle>

{children}
</>
);
}
@@ -0,0 +1 @@
export { FormSection } from './FormSection';
@@ -0,0 +1,75 @@
import { useState } from 'react';
import { useRouter } from '@uirouter/react';
import _ from 'lodash';

import { Button } from '@/portainer/components/Button';
import { PageHeader } from '@/portainer/components/PageHeader';
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
import { r2a } from '@/react-tools/react2angular';

import { EnvironmentSelector, Value } from './EnvironmentSelector';
import { environmentTypes } from './environment-types';

export function EnvironmentTypeSelectView() {
const [types, setTypes] = useState<Value[]>([]);
const { trackEvent } = useAnalytics();
const router = useRouter();

return (
<>
<PageHeader
title="Quick Setup"
breadcrumbs={[{ label: 'Environment Wizard' }]}
/>

<div className="row">
<div className="col-sm-12">
<Widget>
<WidgetTitle icon="fa-magic" title="Environment Wizard" />
<WidgetBody>
<EnvironmentSelector value={types} onChange={setTypes} />

<div className="row">
<div className="col-sm-12">
<Button
disabled={types.length === 0}
onClick={() => startWizard()}
>
Start Wizard
</Button>
</div>
</div>
</WidgetBody>
</Widget>
</div>
</div>
</>
);

function startWizard() {
if (types.length === 0) {
return;
}

const steps = _.compact(
types.map((id) => environmentTypes.find((eType) => eType.id === id))
);

trackEvent('endpoint-wizard-endpoint-select', {
category: 'portainer',
metadata: {
environment: steps.map((step) => step.title).join('/'),
},
});

router.stateService.go('portainer.wizard.endpoints.create', {
envType: types,
});
}
}

export const EnvironmentTypeSelectViewAngular = r2a(
EnvironmentTypeSelectView,
[]
);
@@ -0,0 +1,51 @@
import { FormSection } from '@/portainer/components/form-components/FormSection';
import { r2a } from '@/react-tools/react2angular';

import { EnvironmentSelectorItem } from './EnvironmentSelectorItem';
import { environmentTypes } from './environment-types';

export type Value = typeof environmentTypes[number]['id'];

interface Props {
value: Value[];
onChange(value: Value[]): void;
}

export function EnvironmentSelector({ value, onChange }: Props) {
return (
<div className="row">
<FormSection title="Select your environment(s)">
<p className="text-muted small">
You can onboard different types of environments, select all that
apply.
</p>
<div className="col-sm-12">
{environmentTypes.map((eType) => (
<EnvironmentSelectorItem
key={eType.id}
title={eType.title}
description={eType.description}
icon={eType.icon}
active={value.includes(eType.id)}
onClick={() => handleClick(eType.id)}
/>
))}
</div>
</FormSection>
</div>
);

function handleClick(eType: Value) {
if (value.includes(eType)) {
onChange(value.filter((v) => v !== eType));
return;
}

onChange([...value, eType]);
}
}

export const EnvironmentSelectorAngular = r2a(EnvironmentSelector, [
'value',
'onChange',
]);
@@ -0,0 +1,38 @@
.root {
--selected-item-color: var(--blue-2);
display: block;
width: 200px;
height: 300px;
border: 1px solid rgb(163, 163, 163);
border-radius: 5px;
float: left;
margin-right: 15px;
padding: 25px 20px;
cursor: pointer;
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 60%);
}

.root:hover {
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
border: 1px solid var(--blue-2);
color: #337ab7;
}

.active:hover {
color: #fff;
}

.active {
background: #337ab7;
color: #fff;
border: 1px solid var(--blue-2);
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
}

.icon {
font-size: 80px;
}

.icon-component {
font-size: 40px;
}
@@ -0,0 +1,46 @@
import clsx from 'clsx';
import { ComponentType } from 'react';

import styles from './EnvironmentSelectorItem.module.css';

export interface SelectorItemType {
icon: string | ComponentType<{ selected?: boolean; className?: string }>;
title: string;
description: string;
}

interface Props extends SelectorItemType {
active: boolean;
onClick(): void;
}

export function EnvironmentSelectorItem({
icon,
active,
description,
title,
onClick,
}: Props) {
const Icon = typeof icon !== 'string' ? icon : null;

return (
<button
className={clsx('border-0', styles.root, { [styles.active]: active })}
type="button"
onClick={onClick}
>
<div className="text-center mt-2">
{Icon ? (
<Icon selected={active} className={styles.iconComponent} />
) : (
<i className={clsx(icon, 'block', styles.icon)} />
)}
</div>

<div className="mt-3 text-center">
<h3>{title}</h3>
<h5>{description}</h5>
</div>
</button>
);
}
@@ -0,0 +1,13 @@
.selected .mask-icon {
color: var(--selected-item-color);
}

:global(:root[theme='highcontrast']) .mask-icon,
:global(:root[theme='dark']) .mask-icon {
color: var(--bg-boxselector-wrapper-disabled-color);
}

.mask-icon {
color: var(--bg-boxselector-color);
transform: scale(1.2);
}
21 changes: 21 additions & 0 deletions app/portainer/views/wizard/EnvironmentTypeSelectView/KaaSIcon.tsx
@@ -0,0 +1,21 @@
import clsx from 'clsx';

import styles from './KaaSIcon.module.css';

interface Props {
selected?: boolean;
className?: string;
}

export function KaaSIcon({ selected, className }: Props) {
return (
<span
className={clsx('fa-stack fa-1x', styles.root, className, {
[styles.selected]: selected,
})}
>
<i className="fas fa-cloud fa-stack-2x" />
<i className={clsx('fas fa-dharmachakra fa-stack-1x', styles.maskIcon)} />
</span>
);
}
@@ -0,0 +1,35 @@
import { KaaSIcon } from './KaaSIcon';

export const environmentTypes = [
{
id: 'docker',
title: 'Docker',
icon: 'fab fa-docker',
description:
'Connect to Docker Standalone / Swarm via URL/IP, API or Socket',
},
{
id: 'kubernetes',
title: 'Kubernetes',
icon: 'fas fa-dharmachakra',
description: 'Connect to a kubernetes environment via URL/IP',
},
{
id: 'kaas',
title: 'KaaS',
description: 'Provision a Kubernetes environment with a cloud provider',
icon: KaaSIcon,
},
{
id: 'aci',
title: 'ACI',
description: 'Connect to ACI environment via API',
icon: 'fab fa-microsoft',
},
{
id: 'nomad',
title: 'Nomad',
description: 'Connect to HashiCorp Nomad environment via API',
icon: 'nomad-icon',
},
] as const;

0 comments on commit 7132c7f

Please sign in to comment.