diff --git a/app/angulartics.matomo/analytics-services.ts b/app/angulartics.matomo/analytics-services.ts index 6141a4da60f2e..85a4a267cd3c7 100644 --- a/app/angulartics.matomo/analytics-services.ts +++ b/app/angulartics.matomo/analytics-services.ts @@ -1,5 +1,7 @@ import _ from 'lodash'; +import { useSettings } from '@/portainer/settings/settings.service'; + const categories = [ 'docker', 'kubernetes', @@ -61,6 +63,18 @@ export function push( } } +export function useAnalytics() { + const telemetryQuery = useSettings((settings) => settings.EnableTelemetry); + + return { trackEvent: handleTrackEvent }; + + function handleTrackEvent(...args: Parameters) { + 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 diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 71996b2ccca74..5fa6586c964d9 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -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', @@ -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); diff --git a/app/portainer/components/form-components/FormSection/FormSection.stories.tsx b/app/portainer/components/form-components/FormSection/FormSection.stories.tsx new file mode 100644 index 0000000000000..b0aa7b34c16de --- /dev/null +++ b/app/portainer/components/form-components/FormSection/FormSection.stories.tsx @@ -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 {content}; +} + +export const Example: Story = 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. + `, +}; diff --git a/app/portainer/components/form-components/FormSection/FormSection.tsx b/app/portainer/components/form-components/FormSection/FormSection.tsx new file mode 100644 index 0000000000000..b3230471bd548 --- /dev/null +++ b/app/portainer/components/form-components/FormSection/FormSection.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren } from 'react'; + +import { FormSectionTitle } from '../FormSectionTitle'; + +interface Props { + title: string; +} + +export function FormSection({ title, children }: PropsWithChildren) { + return ( + <> + {title} + + {children} + + ); +} diff --git a/app/portainer/components/form-components/FormSection/index.ts b/app/portainer/components/form-components/FormSection/index.ts new file mode 100644 index 0000000000000..f9b071ee821a7 --- /dev/null +++ b/app/portainer/components/form-components/FormSection/index.ts @@ -0,0 +1 @@ +export { FormSection } from './FormSection'; diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/EndpointTypeView.tsx b/app/portainer/views/wizard/EnvironmentTypeSelectView/EndpointTypeView.tsx new file mode 100644 index 0000000000000..3472361fbf6f1 --- /dev/null +++ b/app/portainer/views/wizard/EnvironmentTypeSelectView/EndpointTypeView.tsx @@ -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([]); + const { trackEvent } = useAnalytics(); + const router = useRouter(); + + return ( + <> + + +
+
+ + + + + +
+
+ +
+
+
+
+
+
+ + ); + + 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, + [] +); diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelector.tsx b/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelector.tsx new file mode 100644 index 0000000000000..9bdfc92fa28c8 --- /dev/null +++ b/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelector.tsx @@ -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 ( +
+ +

+ You can onboard different types of environments, select all that + apply. +

+
+ {environmentTypes.map((eType) => ( + handleClick(eType.id)} + /> + ))} +
+
+
+ ); + + 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', +]); diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelectorItem.module.css b/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelectorItem.module.css new file mode 100644 index 0000000000000..55f658e098ae7 --- /dev/null +++ b/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelectorItem.module.css @@ -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; +} diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelectorItem.tsx b/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelectorItem.tsx new file mode 100644 index 0000000000000..77a2acbb95e7e --- /dev/null +++ b/app/portainer/views/wizard/EnvironmentTypeSelectView/EnvironmentSelectorItem.tsx @@ -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 ( + + ); +} diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/KaaSIcon.module.css b/app/portainer/views/wizard/EnvironmentTypeSelectView/KaaSIcon.module.css new file mode 100644 index 0000000000000..3c47c96601d7f --- /dev/null +++ b/app/portainer/views/wizard/EnvironmentTypeSelectView/KaaSIcon.module.css @@ -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); +} diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/KaaSIcon.tsx b/app/portainer/views/wizard/EnvironmentTypeSelectView/KaaSIcon.tsx new file mode 100644 index 0000000000000..68e9fd38e8e64 --- /dev/null +++ b/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 ( + + + + + ); +} diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/environment-types.ts b/app/portainer/views/wizard/EnvironmentTypeSelectView/environment-types.ts new file mode 100644 index 0000000000000..a5f78b84df684 --- /dev/null +++ b/app/portainer/views/wizard/EnvironmentTypeSelectView/environment-types.ts @@ -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; diff --git a/app/portainer/views/wizard/EnvironmentTypeSelectView/index.ts b/app/portainer/views/wizard/EnvironmentTypeSelectView/index.ts new file mode 100644 index 0000000000000..0aa3c7441b237 --- /dev/null +++ b/app/portainer/views/wizard/EnvironmentTypeSelectView/index.ts @@ -0,0 +1,4 @@ +export { + EnvironmentTypeSelectView, + EnvironmentTypeSelectViewAngular, +} from './EndpointTypeView'; diff --git a/app/portainer/views/wizard/index.js b/app/portainer/views/wizard/index.js index 4926f47aa2cb3..c108e5800877c 100644 --- a/app/portainer/views/wizard/index.js +++ b/app/portainer/views/wizard/index.js @@ -1,8 +1,51 @@ import angular from 'angular'; import { environmentCreationViewModule } from './EnvironmentsCreationView'; +import { EnvironmentTypeSelectViewAngular } from './EnvironmentTypeSelectView'; import controller from './wizard-view.controller.js'; -export const wizardModule = angular.module('portainer.app.wizard', [environmentCreationViewModule]).component('wizardView', { - templateUrl: './wizard-view.html', - controller, -}).name; +export const wizardModule = angular + .module('portainer.app.wizard', [environmentCreationViewModule]) + .component('wizardView', { + templateUrl: './wizard-view.html', + controller, + }) + .component('wizardEnvironmentTypeSelectView', EnvironmentTypeSelectViewAngular) + .config(config).name; + +function config($stateRegistryProvider) { + $stateRegistryProvider.register({ + name: 'portainer.wizard', + url: '/wizard', + views: { + 'content@': { + component: 'wizardView', + }, + }, + }); + + $stateRegistryProvider.register({ + name: 'portainer.wizard.endpoints.create', + url: '/create?envType', + views: { + 'content@': { + component: 'wizardEndpoints', + }, + }, + params: { + envType: '', + }, + }); + + $stateRegistryProvider.register({ + name: 'portainer.wizard.endpoints', + url: '/endpoints', + views: { + 'content@': { + component: 'wizardEnvironmentTypeSelectView', + }, + }, + params: { + localEndpointId: 0, + }, + }); +} diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js index 9fe05b5424855..4e0dac26abbeb 100644 --- a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js +++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.controller.js @@ -218,8 +218,17 @@ export default class WizardEndpointsController { }), (this.endpoints = []); - const endpoints = await this.EndpointService.endpoints(); - this.endpoints = endpoints.value; + const { envType } = this.$state.params; + + if (!envType) { + this.$state.go('portainer.wizard.endpoints'); + } + + const envTypes = Array.isArray(envType) ? envType : [envType]; + envTypes.forEach((type) => this.endpointSelect(type)); + this.startWizard(); + + this.reloadEndpoints(); }); } } diff --git a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html index 2776564ffb1a6..e2a76ea5bcb0d 100644 --- a/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html +++ b/app/portainer/views/wizard/wizard-endpoints/wizard-endpoints.html @@ -3,7 +3,7 @@ Environment Wizard -
+
@@ -43,54 +43,7 @@
-
- -
-
- -
-
- - - -
-
Select your environment(s)
-
- You can onboard different types of environments, select all that apply. -
-
- - - - - -
- -
-
- -
-
-
-
-
+
+