From 149c9a857a0b66cf9169a4269839aace4a71c083 Mon Sep 17 00:00:00 2001 From: Kay Roepke Date: Fri, 5 Jun 2026 15:20:48 +0200 Subject: [PATCH 1/4] first wip for collector onboarding ui --- .../hooks/useCollectorsMutations.ts | 8 +- .../overview/CollectorsOverview.tsx | 44 ++-- .../overview/FirstOnboarding.test.tsx | 186 +++++++++++++++ .../collectors/overview/FirstOnboarding.tsx | 212 ++++++++++++++++++ .../overview/onboarding/ConnectionSuccess.tsx | 179 +++++++++++++++ .../overview/onboarding/FleetSelector.tsx | 80 +++++++ .../onboarding/InstallCommand.test.tsx | 45 ++++ .../overview/onboarding/InstallCommand.tsx | 87 +++++++ .../onboarding/PlatformPicker.test.tsx | 59 +++++ .../overview/onboarding/PlatformPicker.tsx | 147 ++++++++++++ .../onboarding/WaitingForConnection.tsx | 90 ++++++++ .../overview/onboarding/defaultSources.ts | 55 +++++ .../overview/onboarding/platforms.ts | 77 +++++++ 13 files changed, 1247 insertions(+), 22 deletions(-) create mode 100644 graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.test.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/ConnectionSuccess.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/FleetSelector.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/InstallCommand.test.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/InstallCommand.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/PlatformPicker.test.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/PlatformPicker.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/WaitingForConnection.tsx create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/defaultSources.ts create mode 100644 graylog2-web-interface/src/components/collectors/overview/onboarding/platforms.ts diff --git a/graylog2-web-interface/src/components/collectors/hooks/useCollectorsMutations.ts b/graylog2-web-interface/src/components/collectors/hooks/useCollectorsMutations.ts index fbfc3696843e..9e390f2c73d1 100644 --- a/graylog2-web-interface/src/components/collectors/hooks/useCollectorsMutations.ts +++ b/graylog2-web-interface/src/components/collectors/hooks/useCollectorsMutations.ts @@ -31,6 +31,8 @@ import type { CollectorsConfigRequest, Fleet, Source } from '../types'; type CreateSourceInput = { fleetId: string; source: Omit; + // When true, suppress the per-source success notification (e.g. bulk creation during onboarding). + silent?: boolean; }; type UpdateSourceInput = { fleetId: string; @@ -106,8 +108,10 @@ const useCollectorsMutations = () => { config: { type: source.type, ...source.config }, }) as Promise, onError: onMutationError('Creating source'), - onSuccess: (source) => { - UserNotification.success(`Source "${source.name}" has been created.`, 'Success!'); + onSuccess: (source, { silent }) => { + if (!silent) { + UserNotification.success(`Source "${source.name}" has been created.`, 'Success!'); + } return invalidateCollectorsQueries(); }, diff --git a/graylog2-web-interface/src/components/collectors/overview/CollectorsOverview.tsx b/graylog2-web-interface/src/components/collectors/overview/CollectorsOverview.tsx index e3635db5f7e0..110b35338d2c 100644 --- a/graylog2-web-interface/src/components/collectors/overview/CollectorsOverview.tsx +++ b/graylog2-web-interface/src/components/collectors/overview/CollectorsOverview.tsx @@ -15,24 +15,26 @@ * . */ import * as React from 'react'; -import { useState } from 'react'; -import styled, { css } from 'styled-components'; +import {useState} from 'react'; +import styled, {css} from 'styled-components'; -import { Alert, Input } from 'components/bootstrap'; -import { Spinner } from 'components/common'; +import {Alert, Input} from 'components/bootstrap'; +import {Spinner} from 'components/common'; import useHistory from 'routing/useHistory'; import Routes from 'routing/Routes'; -import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; +import {TELEMETRY_EVENT_TYPE} from 'logic/telemetry/Constants'; import FleetCardsGrid from './FleetCardsGrid'; import RecentActivity from './RecentActivity'; -import { useCollectorStats, useFleetsBulkStats } from '../hooks'; +import {useCollectorStats, useFleetsBulkStats} from '../hooks'; import useSendCollectorsTelemetry from '../hooks/useSendCollectorsTelemetry'; -import StatCard, { type Variant as StatCardVariant } from '../common/StatCard'; +import StatCard, {type Variant as StatCardVariant} from '../common/StatCard'; +import {CollectorStats} from 'components/collectors/types'; +import FirstOnboarding from './FirstOnboarding'; const StatsRow = styled.div( - ({ theme }) => css` + ({theme}) => css` display: flex; margin-bottom: ${theme.spacings.lg}; gap: ${theme.spacings.md}; @@ -41,7 +43,7 @@ const StatsRow = styled.div( ); const SectionHeader = styled.div( - ({ theme }) => css` + ({theme}) => css` display: flex; justify-content: space-between; align-items: center; @@ -50,14 +52,13 @@ const SectionHeader = styled.div( ); const SectionTitle = styled.h3( - ({ theme }) => css` + ({theme}) => css` margin: 0; font-size: ${theme.fonts.size.h3}; `, ); -const StatsSection = () => { - const { data: stats, isLoading, isError } = useCollectorStats(); +const StatsSection = ({stats}: { stats: CollectorStats }) => { const history = useHistory(); const sendTelemetry = useSendCollectorsTelemetry(); @@ -85,10 +86,6 @@ const StatsSection = () => { }); }; - if (isLoading) return ; - - if (isError) return Could not load Collector stats.; - return ( { ); }; -const FleetsSection = ({ filter }: { filter: string }) => { - const { data: bulkStats, isLoading, isError } = useFleetsBulkStats(); +const FleetsSection = ({filter}: { filter: string }) => { + const {data: bulkStats, isLoading, isError} = useFleetsBulkStats(); if (isLoading) return ; @@ -150,10 +147,17 @@ const FleetsSection = ({ filter }: { filter: string }) => { const CollectorsOverview = () => { const [filter, setFilter] = useState(''); + const {data: stats, isLoading, isError} = useCollectorStats(); + + if (isLoading) return ; + + if (isError) return Could not load Collector stats.; + + if (stats.total_instances == 0) return ; return (
- + Fleets @@ -162,7 +166,7 @@ const CollectorsOverview = () => { placeholder="Filter fleets..." value={filter} onChange={(e) => setFilter(e.target.value)} - style={{ width: 200, marginBottom: 0 }} + style={{width: 200, marginBottom: 0}} /> diff --git a/graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.test.tsx b/graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.test.tsx new file mode 100644 index 000000000000..5afd84231d6b --- /dev/null +++ b/graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.test.tsx @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { render, screen, waitFor } from 'wrappedTestingLibrary'; +import userEvent from '@testing-library/user-event'; + +import { asMock } from 'helpers/mocking'; + +import FirstOnboarding from './FirstOnboarding'; + +import { useCollectorsConfig, useCollectorsMutations, useFleets } from '../hooks'; +import { mockCollectorsMutations } from '../testing/mockMutations'; + +jest.mock('../hooks'); +jest.mock('util/Version', () => ({ + getMajorAndMinorVersion: () => '7.1', +})); +jest.mock('util/copyToClipboard', () => jest.fn(() => Promise.resolve())); +jest.mock('components/common/Tooltip', () => ({ children }: { children: React.ReactNode }) => <>{children}); +jest.mock('routing/useHistory', () => () => ({ push: jest.fn() })); + +const mockConfig = { + http: { hostname: 'graylog.example', port: 4317 }, + ca_cert_id: null, + signing_cert_id: null, + token_signing_key: null, + otlp_server_cert_id: null, + collector_offline_threshold: 'PT5M', + collector_default_visibility_threshold: 'PT1H', + collector_expiration_threshold: 'P30D', +}; + +const mockFleets = [ + { id: 'fleet-1', name: 'Default Fleet', created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' }, +]; + +const multipleFleets = [ + { id: 'fleet-1', name: 'Production', created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' }, + { id: 'fleet-2', name: 'Staging', created_at: '2026-01-02T00:00:00Z', updated_at: '2026-01-02T00:00:00Z' }, +]; + +describe('FirstOnboarding', () => { + const createEnrollmentToken = jest.fn(); + const createFleet = jest.fn(); + const createSource = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + asMock(useCollectorsConfig).mockReturnValue({ data: mockConfig, isLoading: false }); + asMock(useFleets).mockReturnValue({ data: mockFleets, isLoading: false }); + asMock(useCollectorsMutations).mockReturnValue( + mockCollectorsMutations({ createEnrollmentToken, createFleet, createSource }), + ); + createEnrollmentToken.mockResolvedValue({ + token: 'test-token-abc', + fleet_id: 'fleet-1', + expires_at: '2026-06-04T00:00:00Z', + }); + createFleet.mockResolvedValue({ + id: 'new-fleet-id', + name: 'Onboarding - 2026-05-28', + created_at: '2026-05-28T00:00:00Z', + updated_at: '2026-05-28T00:00:00Z', + }); + createSource.mockResolvedValue({}); + }); + + it('renders the platform picker initially', () => { + render(); + + expect(screen.getByText(/deploy lightweight collectors/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /linux/i })).toBeInTheDocument(); + }); + + it('auto-selects the single fleet and shows install command', async () => { + render(); + + await userEvent.click(screen.getByRole('button', { name: /linux/i })); + + await waitFor(() => { + expect(screen.getByText(/waiting for connection/i)).toBeInTheDocument(); + }); + + expect(createFleet).not.toHaveBeenCalled(); + expect(createEnrollmentToken).toHaveBeenCalledWith({ + name: 'onboarding', + fleetId: 'fleet-1', + expiresIn: 'P7D', + }); + + expect(screen.getByText(/test-token-abc/)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /linux/i })).toBeInTheDocument(); + }); + + it('creates an onboarding fleet when no fleets exist', async () => { + asMock(useFleets).mockReturnValue({ data: [], isLoading: false }); + + render(); + + await userEvent.click(screen.getByRole('button', { name: /linux/i })); + + await waitFor(() => { + expect(createFleet).toHaveBeenCalledWith( + expect.objectContaining({ + name: expect.stringContaining('Onboarding'), + description: expect.stringContaining('7.1'), + }), + ); + }); + + expect(createSource).toHaveBeenCalledTimes(3); + + await waitFor(() => { + expect(createEnrollmentToken).toHaveBeenCalledWith({ + name: 'onboarding', + fleetId: 'new-fleet-id', + expiresIn: 'P7D', + }); + }); + }); + + it('shows fleet selector when multiple fleets exist', () => { + asMock(useFleets).mockReturnValue({ data: multipleFleets, isLoading: false }); + + render(); + + expect(screen.getByText(/choose a fleet/i)).toBeInTheDocument(); + }); + + it('reuses the token when switching platforms', async () => { + render(); + + await userEvent.click(screen.getByRole('button', { name: /linux/i })); + + await waitFor(() => { + expect(screen.getByText(/waiting for connection/i)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button', { name: /windows/i })); + + await waitFor(() => { + expect(screen.getByText(/install on windows/i)).toBeInTheDocument(); + }); + + expect(createEnrollmentToken).toHaveBeenCalledTimes(1); + }); + + it('transitions to connected phase after simulating connection', async () => { + render(); + + await userEvent.click(screen.getByRole('button', { name: /linux/i })); + + await waitFor(() => { + expect(screen.getByText(/waiting for connection/i)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByRole('button', { name: /simulate connection/i })); + + expect(screen.getByText(/collector connected/i)).toBeInTheDocument(); + // web-prod-01 appears twice: as the connection hostname and as an auto-detected host asset + expect(screen.getAllByText(/web-prod-01/i).length).toBeGreaterThan(0); + }); + + it('shows spinner while config or fleets are loading', async () => { + asMock(useCollectorsConfig).mockReturnValue({ data: undefined, isLoading: true }); + + render(); + + // Spinner renders behind a 200ms Delayed wrapper, so wait for it to appear + expect(await screen.findByText(/loading/i)).toBeInTheDocument(); + }); +}); diff --git a/graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.tsx b/graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.tsx new file mode 100644 index 000000000000..fc0e71f06141 --- /dev/null +++ b/graylog2-web-interface/src/components/collectors/overview/FirstOnboarding.tsx @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { useState, useCallback, useRef } from 'react'; +import styled, { css } from 'styled-components'; + +import { Spinner } from 'components/common'; +import { getMajorAndMinorVersion } from 'util/Version'; + +import PlatformPicker from './onboarding/PlatformPicker'; +import InstallCommand from './onboarding/InstallCommand'; +import WaitingForConnection from './onboarding/WaitingForConnection'; +import ConnectionSuccess from './onboarding/ConnectionSuccess'; +import FleetSelector from './onboarding/FleetSelector'; +import PLATFORMS from './onboarding/platforms'; +import type { PlatformId } from './onboarding/platforms'; +import DEFAULT_SOURCES from './onboarding/defaultSources'; + +import { useCollectorsConfig, useCollectorsMutations, useFleets } from '../hooks'; + +type Phase = 'pick' | 'waiting' | 'connected'; + +// The platform picker (700px) and fleet selector (400px) own their own centered widths. +// The install/connected body gets a wider column so the command, stat cards, and asset grid have room. +const BodyContainer = styled.div( + ({ theme }) => css` + max-width: 960px; + margin: 0 auto ${theme.spacings.lg}; + `, +); + +const formatDate = () => { + const now = new Date(); + + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; +}; + +const FirstOnboarding = () => { + const [phase, setPhase] = useState('pick'); + const [selectedPlatform, setSelectedPlatform] = useState(null); + const [installCommand, setInstallCommand] = useState(''); + const [selectedFleetId, setSelectedFleetId] = useState(null); + const [createNewFleet, setCreateNewFleet] = useState(false); + + const tokenRef = useRef(null); + const fleetIdRef = useRef(null); + + const { data: config, isLoading: isConfigLoading } = useCollectorsConfig(); + const { data: fleets, isLoading: isFleetsLoading } = useFleets(); + const { + createFleet, isCreatingFleet, + createSource, + createEnrollmentToken, isCreatingEnrollmentToken, + } = useCollectorsMutations(); + + const buildCommand = useCallback( + (platformId: PlatformId, token: string) => { + if (!config) return ''; + + const platform = PLATFORMS.find((p) => p.id === platformId); + + if (!platform) return ''; + + return platform.commandTemplate(config.http.hostname, config.http.port, token); + }, + [config], + ); + + const createOnboardingFleet = useCallback(async () => { + const version = getMajorAndMinorVersion(); + const fleet = await createFleet({ + name: `Onboarding - ${formatDate()}`, + description: `Created by Graylog ${version} onboarding wizard`, + }); + + await Promise.all( + DEFAULT_SOURCES.map((source) => createSource({ fleetId: fleet.id, source, silent: true })), + ); + + return fleet.id; + }, [createFleet, createSource]); + + const resolveFleetId = useCallback(async (): Promise => { + if (fleetIdRef.current) return fleetIdRef.current; + + if (!fleets) return null; + + if (fleets.length === 0 || createNewFleet) { + const newFleetId = await createOnboardingFleet(); + fleetIdRef.current = newFleetId; + + return newFleetId; + } + + if (fleets.length === 1) { + fleetIdRef.current = fleets[0].id; + + return fleets[0].id; + } + + if (selectedFleetId) { + fleetIdRef.current = selectedFleetId; + + return selectedFleetId; + } + + return null; + }, [fleets, createNewFleet, selectedFleetId, createOnboardingFleet]); + + const handleFleetSelect = useCallback((fleetId: string | null, isCreateNew: boolean) => { + setSelectedFleetId(fleetId); + setCreateNewFleet(isCreateNew); + fleetIdRef.current = null; + tokenRef.current = null; + }, []); + + const handlePlatformSelect = useCallback( + async (platformId: PlatformId) => { + if (!config) return; + + setSelectedPlatform(platformId); + + if (tokenRef.current) { + setInstallCommand(buildCommand(platformId, tokenRef.current)); + setPhase('waiting'); + + return; + } + + try { + const fleetId = await resolveFleetId(); + + if (!fleetId) return; + + const response = await createEnrollmentToken({ + name: 'onboarding', + fleetId, + expiresIn: 'P1D', + }); + + tokenRef.current = response.token; + setInstallCommand(buildCommand(platformId, response.token)); + setPhase('waiting'); + } catch { + // Error notification handled by useCollectorsMutations onError callback + } + }, + [config, resolveFleetId, createEnrollmentToken, buildCommand], + ); + + const handleSimulateConnection = useCallback(() => { + setPhase('connected'); + }, []); + + if (isConfigLoading || isFleetsLoading) return ; + + const isBusy = isCreatingFleet || isCreatingEnrollmentToken; + const showFleetSelector = (fleets?.length ?? 0) > 1; + const needsFleetSelection = showFleetSelector && !selectedFleetId && !createNewFleet; + + return ( +
+ {showFleetSelector && ( + + )} + + + + {phase !== 'pick' && selectedPlatform && ( + + {phase === 'waiting' && ( + <> + p.id === selectedPlatform)?.label ?? ''} + tokenDuration='P1D' + /> + + + )} + + {phase === 'connected' && } + + )} +
+ ); +}; + +export default FirstOnboarding; diff --git a/graylog2-web-interface/src/components/collectors/overview/onboarding/ConnectionSuccess.tsx b/graylog2-web-interface/src/components/collectors/overview/onboarding/ConnectionSuccess.tsx new file mode 100644 index 000000000000..e016ff104c9e --- /dev/null +++ b/graylog2-web-interface/src/components/collectors/overview/onboarding/ConnectionSuccess.tsx @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import styled, { css } from 'styled-components'; + +import { Alert, Label } from 'components/bootstrap'; +import { AccessibleCard } from 'components/common'; +import useHistory from 'routing/useHistory'; +import Routes from 'routing/Routes'; + +import type { PlatformId } from './platforms'; +import PLATFORMS from './platforms'; + +import StatCard from '../../common/StatCard'; + +type Props = { + platformId: PlatformId; +}; + +const MOCK_CONNECTION = { + hostname: 'web-prod-01', + version: 'v1.0.0', + fleetName: 'Default Fleet', + sources: ['syslog', 'auth.log'], + assets: [ + { type: 'host', name: 'web-prod-01' }, + { type: 'user', name: 'root' }, + ], + messageCount: 142, +}; + +const SummaryRow = styled.div( + ({ theme }) => css` + display: flex; + gap: ${theme.spacings.sm}; + flex-wrap: wrap; + margin-bottom: ${theme.spacings.lg}; + `, +); + +const StatsRow = styled.div( + ({ theme }) => css` + display: flex; + gap: ${theme.spacings.md}; + flex-wrap: wrap; + margin-bottom: ${theme.spacings.lg}; + `, +); + +const SectionTitle = styled.h3( + ({ theme }) => css` + font-size: ${theme.fonts.size.h3}; + margin: 0 0 ${theme.spacings.sm} 0; + `, +); + +const AssetsGrid = styled.div( + ({ theme }) => css` + display: flex; + gap: ${theme.spacings.md}; + flex-wrap: wrap; + margin-bottom: ${theme.spacings.lg}; + `, +); + +const AssetCard = styled(AccessibleCard)( + ({ theme }) => css` + min-width: 150px; + padding: ${theme.spacings.md}; + `, +); + +const AssetType = styled.div( + ({ theme }) => css` + font-size: ${theme.fonts.size.small}; + color: ${theme.colors.gray[60]}; + text-transform: uppercase; + margin-bottom: ${theme.spacings.xxs}; + `, +); + +const AssetName = styled.div` + font-weight: 500; +`; + +const NextGrid = styled.div( + ({ theme }) => css` + display: flex; + gap: ${theme.spacings.md}; + flex-wrap: wrap; + `, +); + +const NextCard = styled(AccessibleCard)( + ({ theme }) => css` + flex: 1; + min-width: 180px; + padding: ${theme.spacings.md}; + + h4 { + margin: 0 0 ${theme.spacings.xxs} 0; + font-size: ${theme.fonts.size.large}; + } + + p { + margin: 0; + font-size: ${theme.fonts.size.small}; + color: ${theme.colors.gray[60]}; + } + `, +); + +const ConnectionSuccess = ({ platformId }: Props) => { + const history = useHistory(); + const platform = PLATFORMS.find((p) => p.id === platformId); + + return ( +
+ + Collector connected — {MOCK_CONNECTION.hostname} running{' '} + {MOCK_CONNECTION.version} + + + + + + + + + + + + + + + Auto-detected assets + + {MOCK_CONNECTION.assets.map((asset) => ( + + {asset.type} + {asset.name} + + ))} + + + What's next? + + history.push(Routes.SYSTEM.COLLECTORS.FLEETS)}> +

Manage Fleets

+

Group collectors by environment or team.

+
+ history.push(Routes.SYSTEM.COLLECTORS.FLEETS)}> +

Configure Sources

+

Add file paths, journald, or Windows Event Log sources.

+
+ history.push(Routes.SYSTEM.COLLECTORS.INSTANCES)}> +

View Instances

+

Monitor all connected collector instances.

+
+
+
+ ); +}; + +export default ConnectionSuccess; diff --git a/graylog2-web-interface/src/components/collectors/overview/onboarding/FleetSelector.tsx b/graylog2-web-interface/src/components/collectors/overview/onboarding/FleetSelector.tsx new file mode 100644 index 000000000000..a7fec4b10756 --- /dev/null +++ b/graylog2-web-interface/src/components/collectors/overview/onboarding/FleetSelector.tsx @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import styled, { css } from 'styled-components'; + +import { Select } from 'components/common'; + +import type { Fleet } from '../../types'; + +const CREATE_NEW_VALUE = '__create_new__'; + +type Props = { + fleets: Fleet[]; + selectedFleetId: string | null; + onSelect: (fleetId: string | null, createNew: boolean) => void; + disabled: boolean; +}; + +const Container = styled.div( + ({ theme }) => css` + max-width: 400px; + margin: 0 auto ${theme.spacings.md}; + text-align: left; + `, +); + +const Label = styled.label( + ({ theme }) => css` + display: block; + font-weight: 500; + margin-bottom: ${theme.spacings.xs}; + text-align: center; + `, +); + +const FleetSelector = ({ fleets, selectedFleetId, onSelect, disabled }: Props) => { + const options = [ + ...fleets.map((f) => ({ label: f.name, value: f.id })), + { label: 'Create new onboarding fleet', value: CREATE_NEW_VALUE }, + ]; + + const handleChange = (value: string) => { + if (value === CREATE_NEW_VALUE) { + onSelect(null, true); + } else { + onSelect(value, false); + } + }; + + return ( + + + onSelect({ kind: 'existing', fleetId: value })} + clearable={false} + disabled={disabled} + placeholder="Select existing fleet..." + /> + + + + ); +}; + +export default FleetChoice; diff --git a/graylog2-web-interface/src/components/collectors/overview/onboarding/FleetSelector.tsx b/graylog2-web-interface/src/components/collectors/overview/onboarding/FleetSelector.tsx deleted file mode 100644 index a7fec4b10756..000000000000 --- a/graylog2-web-interface/src/components/collectors/overview/onboarding/FleetSelector.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2020 Graylog, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * . - */ -import * as React from 'react'; -import styled, { css } from 'styled-components'; - -import { Select } from 'components/common'; - -import type { Fleet } from '../../types'; - -const CREATE_NEW_VALUE = '__create_new__'; - -type Props = { - fleets: Fleet[]; - selectedFleetId: string | null; - onSelect: (fleetId: string | null, createNew: boolean) => void; - disabled: boolean; -}; - -const Container = styled.div( - ({ theme }) => css` - max-width: 400px; - margin: 0 auto ${theme.spacings.md}; - text-align: left; - `, -); - -const Label = styled.label( - ({ theme }) => css` - display: block; - font-weight: 500; - margin-bottom: ${theme.spacings.xs}; - text-align: center; - `, -); - -const FleetSelector = ({ fleets, selectedFleetId, onSelect, disabled }: Props) => { - const options = [ - ...fleets.map((f) => ({ label: f.name, value: f.id })), - { label: 'Create new onboarding fleet', value: CREATE_NEW_VALUE }, - ]; - - const handleChange = (value: string) => { - if (value === CREATE_NEW_VALUE) { - onSelect(null, true); - } else { - onSelect(value, false); - } - }; - - return ( - - -