From 4bf4fa7236dd2c41fd39ed34ef56d5674bb32300 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 29 Apr 2024 17:15:25 +0200 Subject: [PATCH 1/8] Move `teleport.e/AccessRequests` to `shared/components/AccessRequests` The last commit from teleport.e that includes the original files: gravitational/teleport.e@5ca6881e29d5342eea70201c3f8b365320348c46 --- .../AccessDuration/AccessDurationRequest.tsx | 82 + .../AccessDuration/AccessDurationReview.tsx | 30 + .../AccessDuration/durationOptions.test.ts | 82 + .../AccessDuration/durationOptions.ts | 157 + .../getDurationOptionsFromStartTime.test.ts | 173 + .../AccessRequests/AccessDuration/index.ts | 2 + .../AssumeStartTime/AssumeStartTime.story.tsx | 126 + .../AssumeStartTime/AssumeStartTime.test.tsx | 147 + .../AssumeStartTime/AssumeStartTime.tsx | 217 ++ .../generateTimeDropdown.test.ts | 124 + .../AssumeStartTime/getTimeOptions.test.ts | 136 + .../AssumeStartTime/timeOptions.test.tsx | 32 + .../AssumeStartTime/timeOptions.tsx | 188 + .../RequestCheckout/AdditionalOptions.tsx | 130 + .../RequestCheckout.story.test.tsx | 19 + .../RequestCheckout/RequestCheckout.story.tsx | 198 + .../RequestCheckout/RequestCheckout.test.tsx | 156 + .../RequestCheckout/RequestCheckout.tsx | 670 ++++ .../RequestCheckout/SelectReviewers.tsx | 373 ++ .../RequestCheckout.story.test.tsx.snap | 3394 +++++++++++++++++ .../NewRequest/RequestCheckout/index.ts | 5 + .../RequestCheckout/shield-check.png | Bin 0 -> 38458 bytes .../NewRequest/RequestCheckout/types.ts | 6 + .../NewRequest/RequestCheckout/utils.test.ts | 95 + .../NewRequest/RequestCheckout/utils.ts | 122 + .../NewRequest/ResourceList/Apps.tsx | 271 ++ .../NewRequest/ResourceList/Databases.tsx | 55 + .../NewRequest/ResourceList/Desktops.tsx | 50 + .../NewRequest/ResourceList/Kubes.tsx | 46 + .../NewRequest/ResourceList/Nodes.tsx | 65 + .../ResourceList/ResourceList.story.test.tsx | 47 + .../ResourceList/ResourceList.story.tsx | 151 + .../NewRequest/ResourceList/ResourceList.tsx | 109 + .../NewRequest/ResourceList/Roles.tsx | 30 + .../NewRequest/ResourceList/UserGroups.tsx | 57 + .../ResourceList.story.test.tsx.snap | 2240 +++++++++++ .../NewRequest/ResourceList/index.ts | 1 + .../AccessRequests/NewRequest/index.ts | 4 + .../AccessRequests/NewRequest/resource.ts | 20 + .../RequestList/RequestList.tsx | 72 + .../ReviewRequests/RequestList/index.ts | 1 + .../RequestDelete.story.test.tsx | 25 + .../RequestDelete/RequestDelete.story.tsx | 61 + .../RequestDelete/RequestDelete.tsx | 90 + .../RequestDelete.story.test.tsx.snap | 1242 ++++++ .../RequestView/RequestDelete/index.ts | 2 + .../RequestReview.story.test.tsx | 15 + .../RequestReview/RequestReview.story.tsx | 51 + .../RequestReview/RequestReview.tsx | 337 ++ .../RequestReview.story.test.tsx.snap | 616 +++ .../RequestView/RequestReview/index.ts | 3 + .../RequestView/RequestView.story.test.tsx | 30 + .../RequestView/RequestView.story.tsx | 248 ++ .../RequestView/RequestView.tsx | 685 ++++ .../RequestView/RolesRequested.tsx | 12 + .../RequestView.story.test.tsx.snap | 3350 ++++++++++++++++ .../ReviewRequests/RequestView/index.ts | 3 + .../ReviewRequests/RequestView/types.ts | 35 + .../ReviewRequests/formattedName.ts | 9 + .../AccessRequests/ReviewRequests/index.ts | 3 + .../AccessRequests/Shared/Shared.tsx | 122 + .../components/AccessRequests/Shared/types.ts | 12 + .../components/AccessRequests/Shared/utils.ts | 29 + .../AccessRequests/fixtures/index.ts | 332 ++ 64 files changed, 17195 insertions(+) create mode 100644 web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx create mode 100644 web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx create mode 100644 web/packages/shared/components/AccessRequests/AccessDuration/durationOptions.test.ts create mode 100644 web/packages/shared/components/AccessRequests/AccessDuration/durationOptions.ts create mode 100644 web/packages/shared/components/AccessRequests/AccessDuration/getDurationOptionsFromStartTime.test.ts create mode 100644 web/packages/shared/components/AccessRequests/AccessDuration/index.ts create mode 100644 web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.story.tsx create mode 100644 web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/AssumeStartTime/AssumeStartTime.tsx create mode 100644 web/packages/shared/components/AccessRequests/AssumeStartTime/generateTimeDropdown.test.ts create mode 100644 web/packages/shared/components/AccessRequests/AssumeStartTime/getTimeOptions.test.ts create mode 100644 web/packages/shared/components/AccessRequests/AssumeStartTime/timeOptions.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/AssumeStartTime/timeOptions.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/__snapshots__/RequestCheckout.story.test.tsx.snap create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/index.ts create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/shield-check.png create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/types.ts create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/utils.test.ts create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/utils.ts create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Databases.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Desktops.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Kubes.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Nodes.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/ResourceList.story.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/ResourceList.story.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/ResourceList.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Roles.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/UserGroups.tsx create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/__snapshots__/ResourceList.story.test.tsx.snap create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/ResourceList/index.ts create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/index.ts create mode 100644 web/packages/shared/components/AccessRequests/NewRequest/resource.ts create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestList/RequestList.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestList/index.ts create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.story.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/RequestDelete.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/__snapshots__/RequestDelete.story.test.tsx.snap create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestDelete/index.ts create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.story.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/__snapshots__/RequestReview.story.test.tsx.snap create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/index.ts create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.test.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.story.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RolesRequested.tsx create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/__snapshots__/RequestView.story.test.tsx.snap create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/index.ts create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/types.ts create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/formattedName.ts create mode 100644 web/packages/shared/components/AccessRequests/ReviewRequests/index.ts create mode 100644 web/packages/shared/components/AccessRequests/Shared/Shared.tsx create mode 100644 web/packages/shared/components/AccessRequests/Shared/types.ts create mode 100644 web/packages/shared/components/AccessRequests/Shared/utils.ts create mode 100644 web/packages/shared/components/AccessRequests/fixtures/index.ts diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx new file mode 100644 index 0000000000000..6ae7eb4a649ce --- /dev/null +++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx @@ -0,0 +1,82 @@ +import { useState, useEffect } from 'react'; +import { Flex, LabelInput, Text } from 'design'; +import Select, { Option } from 'shared/components/Select'; +import { ToolTipInfo } from 'shared/components/ToolTip'; + +import { AccessRequest } from 'e-teleport/services/accessRequests'; + +import { + getDurationOptionIndexClosestToOneWeek, + getDurationOptionsFromStartTime, +} from './durationOptions'; + +export function AccessDurationRequest({ + assumeStartTime, + accessRequest, + maxDuration, + setMaxDuration, +}: { + assumeStartTime: Date; + accessRequest: AccessRequest; + maxDuration: Option; + setMaxDuration(s: Option): void; +}) { + // Options for extending or shortening the access request duration. + const [durationOptions, setDurationOptions] = useState[]>([]); + + useEffect(() => { + if (!assumeStartTime) { + defaultDuration(); + } else { + updateAccessDuration(assumeStartTime); + } + }, [assumeStartTime]); + + function defaultDuration() { + const created = accessRequest.created; + const options = getDurationOptionsFromStartTime(created, accessRequest); + + setDurationOptions(options); + if (options.length > 0) { + const durationIndex = getDurationOptionIndexClosestToOneWeek( + options, + accessRequest.created + ); + setMaxDuration(options[durationIndex]); + } + } + + function updateAccessDuration(start: Date) { + const updatedDurationOpts = getDurationOptionsFromStartTime( + start, + accessRequest + ); + + const durationIndex = getDurationOptionIndexClosestToOneWeek( + updatedDurationOpts, + start + ); + + setMaxDuration(updatedDurationOpts[durationIndex]); + setDurationOptions(updatedDurationOpts); + } + + return ( + + + Access Duration + + How long you would be given elevated privileges. Note that the time it + takes to approve this request will be subtracted from the duration you + requested. + + + + ) => setRequestTTL(option)} + value={requestTTL} + /> + + )} + + + Access Request Lifetime + + The max duration of an access request, starting from its + creation, until it expires. + + + + {getFormattedDurationTxt({ + start: dryRunResponse.created, + end: new Date(selectedMaxDurationTimestamp), + })} + + + + )} + + ); +} diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.test.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.test.tsx new file mode 100644 index 0000000000000..1d11ca9ad3d64 --- /dev/null +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { render } from 'design/utils/testing'; + +import { Loaded, Failed, Success } from './RequestCheckout.story'; + +test('loaded state', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +test('failed state', async () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); + +test('success state', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx new file mode 100644 index 0000000000000..9a74e8772d9b8 --- /dev/null +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.story.tsx @@ -0,0 +1,198 @@ +import React, { useState } from 'react'; +import { MemoryRouter, Link } from 'react-router-dom'; +import { Option } from 'shared/components/Select'; +import { Box, ButtonPrimary, ButtonText } from 'design'; + +import { dryRunResponse } from '../../fixtures'; + +import { RequestCheckout, RequestCheckoutProps } from './RequestCheckout'; + +export default { + title: 'TeleportE/AccessRequests/Checkout', +}; + +function SuccessActionComponent({ reset, onClose }) { + return ( + + + Back to Listings + + { + reset(); + onClose(); + }} + > + Make Another Request + + + ); +} + +export const Loaded = () => { + const [selectedReviewers, setSelectedReviewers] = useState( + props.selectedReviewers + ); + const [maxDuration, setMaxDuration] = useState>(); + const [requestTTL, setRequestTTL] = useState>(); + + return ( + + ); +}; +export const Empty = () => { + const [selectedReviewers, setSelectedReviewers] = useState([]); + const [maxDuration, setMaxDuration] = useState>(); + const [requestTTL, setRequestTTL] = useState>(); + + return ( + + ); +}; + +export const Failed = () => ( + +); + +export const LoadedResourceRequest = () => { + const [selectedReviewers, setSelectedReviewers] = useState( + props.selectedReviewers + ); + const [selectedResourceRequestRoles, setSelectedResourceRequestRoles] = + useState(props.resourceRequestRoles); + return ( + + ); +}; + +export const ProcessingResourceRequest = () => ( + +); + +export const FailedResourceRequest = () => ( + +); + +export const Success = () => ( + + + +); + +const props: RequestCheckoutProps = { + createAttempt: { status: '' }, + fetchResourceRequestRolesAttempt: { status: '' }, + isResourceRequest: false, + requireReason: true, + reviewers: ['bob', 'cat', 'george washington'], + selectedReviewers: [ + { value: 'bob', label: 'bob', isSelected: true }, + { value: 'cat', label: 'cat', isSelected: true }, + { + value: 'george washington', + label: 'george washington', + isSelected: true, + }, + ], + setSelectedReviewers: () => null, + createRequest: () => null, + data: [ + { + kind: 'app', + name: 'app-name', + id: 'app-name', + }, + { + kind: 'db', + name: 'app-name', + id: 'app-name', + }, + { + kind: 'kube_cluster', + name: 'kube-name', + id: 'app-name', + }, + { + kind: 'user_group', + name: 'user-group-name', + id: 'app-name', + }, + { + kind: 'windows_desktop', + name: 'desktop-name', + id: 'app-name', + }, + ], + clearAttempt: () => null, + onClose: () => null, + toggleResource: () => null, + reset: () => null, + transitionState: 'entered', + numRequestedResources: 4, + resourceRequestRoles: ['admin', 'access', 'developer'], + selectedResourceRequestRoles: ['admin', 'access'], + setSelectedResourceRequestRoles: () => null, + fetchStatus: 'loaded', + maxDuration: { value: 0, label: '12 hours' }, + setMaxDuration: () => null, + requestTTL: { value: 0, label: '1 hour' }, + setRequestTTL: () => null, + dryRunResponse, +}; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx new file mode 100644 index 0000000000000..ed1a16b5d228d --- /dev/null +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.test.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react'; +import { render, screen, userEvent, fireEvent } from 'design/utils/testing'; +import { Option } from 'shared/components/Select'; + +import { dryRunResponse } from '../../fixtures'; + +import { ReviewerOption } from './types'; + +import { + RequestCheckout as RequestCheckoutComp, + RequestCheckoutProps, +} from './RequestCheckout'; + +test('start with no suggested reviewers', async () => { + render(); + + // Test init renders no reviewers. + let reviewers = screen.getByTestId('reviewers'); + expect(reviewers.childNodes).toHaveLength(0); + + // Add a reviewer + await userEvent.click(screen.getByRole('button', { name: 'Add' })); + await userEvent.type( + screen.getByText(/type or select a name/i), + 'llama{enter}' + ); + await userEvent.click(screen.getByRole('button', { name: 'Done' })); + + reviewers = screen.getByTestId('reviewers'); + expect(reviewers.childNodes).toHaveLength(1); + expect(reviewers.childNodes[0]).toHaveTextContent('llama'); + + // Remove by clicking on x button. + fireEvent.click(reviewers.childNodes[0].lastChild); + reviewers = screen.getByTestId('reviewers'); + expect(reviewers.childNodes).toHaveLength(0); +}); + +test('start with suggested reviewers', async () => { + render(); + + // Test init renders reviewers. + let reviewers = screen.getByTestId('reviewers'); + expect(reviewers.childNodes).toHaveLength(1); + expect(reviewers.childNodes[0]).toHaveTextContent('llama'); + + // Add another reviewer. + await userEvent.click(screen.getByRole('button', { name: 'Edit' })); + await userEvent.type( + screen.getByText(/type or select a name/i), + 'alpaca{enter}' + ); + await userEvent.click(screen.getByRole('button', { name: 'Done' })); + + reviewers = screen.getByTestId('reviewers'); + expect(reviewers.childNodes).toHaveLength(2); + expect(reviewers.childNodes[0]).toHaveTextContent('llama'); + expect(reviewers.childNodes[1]).toHaveTextContent('alpaca'); + + // Remove a suggested reviewer by typing the name. + await userEvent.click(screen.getByRole('button', { name: 'Edit' })); + await userEvent.type( + screen.getByText(/type or select a name/i), + 'llama{enter}' + ); + await userEvent.click(screen.getByRole('button', { name: 'Done' })); + + reviewers = screen.getByTestId('reviewers'); + expect(reviewers.childNodes).toHaveLength(1); + expect(reviewers.childNodes[0]).toHaveTextContent('alpaca'); + + // Suggested reviewer should still be rendered in the dropdown. + await userEvent.click(screen.getByRole('button', { name: 'Edit' })); + await userEvent.click(screen.getByTitle(/llama/i)); + await userEvent.click(screen.getByRole('button', { name: 'Done' })); + + reviewers = screen.getByTestId('reviewers'); + expect(reviewers.childNodes).toHaveLength(2); + expect(reviewers.childNodes[0]).toHaveTextContent('alpaca'); + expect(reviewers.childNodes[1]).toHaveTextContent('llama'); +}); + +test('assume start time + additional info access request lifetime', () => { + jest.useFakeTimers().setSystemTime(dryRunResponse.created); + render(); + + const infoBtn = screen.getByTestId('additional-info-btn'); + + // Init state. + expect(screen.queryByText(/start time/i)).not.toBeInTheDocument(); + expect(screen.getByText(/access duration/i)).toBeInTheDocument(); + expect(screen.getAllByText(/2 days/i)).toHaveLength(1); + const calendarBtn = screen.getByText(/immediately/i); + fireEvent.click(calendarBtn); + + // Expand the additional info box where the access lifetime + // gets displayed. + fireEvent.click(infoBtn); + expect(screen.getByText(/Access Request Lifetime/i)).toBeInTheDocument(); + expect(screen.getAllByText(/2 days/i)).toHaveLength(2); + + // Changing the "access duration" to a shorter time + // should reduce the "access lifetime". + fireEvent.keyDown(screen.getAllByText(/2 days/i)[0], { key: 'ArrowDown' }); + fireEvent.click(screen.getByText(/1 day/i)); + expect(screen.getAllByText(/1 day/i)).toHaveLength(2); +}); + +const RequestCheckout = ({ reviewers = [] }: { reviewers?: string[] }) => { + const [selectedReviewers, setSelectedReviewers] = useState( + () => reviewers.map(r => ({ label: r, value: r, isSelected: true })) + ); + const [maxDuration, setMaxDuration] = useState>(); + + return ( +
+ +
+ ); +}; + +const props: RequestCheckoutProps = { + createAttempt: { status: '' }, + fetchResourceRequestRolesAttempt: { status: '' }, + isResourceRequest: false, + requireReason: true, + reviewers: [], + selectedReviewers: [], + setSelectedReviewers: () => null, + createRequest: () => null, + data: [], + clearAttempt: () => null, + onClose: () => null, + toggleResource: () => null, + reset: () => null, + transitionState: 'entered', + numRequestedResources: 4, + resourceRequestRoles: ['admin', 'access', 'developer'], + selectedResourceRequestRoles: ['admin', 'access'], + setSelectedResourceRequestRoles: () => null, + fetchStatus: 'loaded', + maxDuration: { value: 0, label: '12 hours' }, + setMaxDuration: () => null, + requestTTL: { value: 0, label: '1 hour' }, + setRequestTTL: () => null, + dryRunResponse, +}; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx new file mode 100644 index 0000000000000..f4be7d6da47da --- /dev/null +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx @@ -0,0 +1,670 @@ +import React, { useRef, useState } from 'react'; +import styled from 'styled-components'; +import { + Alert, + Box, + ButtonIcon, + ButtonPrimary, + Flex, + Image, + Indicator, + LabelInput, + Text, +} from 'design'; +import { + ArrowBack, + ChevronDown, + ChevronRight, + Trash, + Warning, +} from 'design/Icon'; +import Table, { Cell } from 'design/DataTable'; +import { CheckboxInput, CheckboxWrapper } from 'design/Checkbox'; +import Validation, { useRule, Validator } from 'shared/components/Validation'; +import { Attempt } from 'shared/hooks/useAttemptNext'; +import { pluralize } from 'shared/utils/text'; +import { Danger } from 'design/Alert'; +import { Option } from 'shared/components/Select'; + +import { CreateRequest } from '../../Shared/types'; +import { AssumeStartTime } from '../../AssumeStartTime/AssumeStartTime'; +import { AccessDurationRequest } from '../../AccessDuration'; + +import { ReviewerOption } from './types'; + +import shieldCheck from './shield-check.png'; +import { SelectReviewers } from './SelectReviewers'; +import { AdditionalOptions } from './AdditionalOptions'; + +import type { TransitionStatus } from 'react-transition-group'; + +import type { AccessRequest } from 'e-teleport/services/accessRequests'; +import type { ResourceKind } from '../resource'; + +export function RequestCheckout({ + toggleResource, + onClose, + transitionState, + reset, + data, + createAttempt, + appsGrantedByUserGroup = [], + userGroupFetchAttempt, + fetchResourceRequestRolesAttempt, + resourceRequestRoles, + createRequest, + clearAttempt, + reviewers, + selectedReviewers, + setSelectedReviewers, + SuccessComponent, + requireReason, + numRequestedResources, + isResourceRequest, + selectedResourceRequestRoles, + setSelectedResourceRequestRoles, + fetchStatus, + maxDuration, + setMaxDuration, + requestTTL, + setRequestTTL, + dryRunResponse, +}: RequestCheckoutProps) { + // Specifies the start date/time a requestor requested for. + const [start, setStart] = useState(); + const [reason, setReason] = useState(''); + const ref = useRef(); + + const isInvalidRoleSelection = + resourceRequestRoles.length > 0 && + isResourceRequest && + selectedResourceRequestRoles.length < 1; + const submitBtnDisabled = + data.length === 0 || + createAttempt.status === 'processing' || + isInvalidRoleSelection || + fetchResourceRequestRolesAttempt.status === 'failed' || + fetchResourceRequestRolesAttempt.status === 'processing'; + + function updateReason(reason: string) { + setReason(reason); + } + + function handleOnSubmit(validator: Validator) { + if (!validator.validate()) { + return; + } + + createRequest({ + reason, + suggestedReviewers: selectedReviewers.map(r => r.value), + maxDuration: maxDuration ? new Date(maxDuration.value) : null, + requestTTL: requestTTL ? new Date(requestTTL.value) : null, + start: start, + }); + } + + // Listeners are attached to enable overflow on the parent container after + // transitioning ends (entered) or starts (exits). Enables vertical scrolling + // when content gets too big. + // + // Overflow is initially hidden to prevent + // brief flashing of horizontal scroll bar resulting from positioning + // the container off screen to the right for the slide affect. + React.useEffect(() => { + function applyOverflowAutoStyle(e: TransitionEvent) { + if (e.propertyName === 'right') { + ref.current.style.overflow = `auto`; + // There will only ever be one 'end right' transition invoked event, so we remove it + // afterwards, and listen for the 'start right' transition which is only invoked + // when user exits this component. + window.removeEventListener('transitionend', applyOverflowAutoStyle); + window.addEventListener('transitionstart', applyOverflowHiddenStyle); + } + } + + function applyOverflowHiddenStyle(e: TransitionEvent) { + if (e.propertyName === 'right') { + ref.current.style.overflow = `hidden`; + } + } + + window.addEventListener('transitionend', applyOverflowAutoStyle); + + return () => { + window.removeEventListener('transitionend', applyOverflowAutoStyle); + window.removeEventListener('transitionstart', applyOverflowHiddenStyle); + }; + }, []); + + return ( +
+ + + {fetchResourceRequestRolesAttempt.status === 'failed' && ( + + )} + {fetchStatus === 'loading' && ( + + + + )} + + {fetchStatus === 'loaded' && ( +
+ {createAttempt.status === 'success' ? ( + + + + Resources Requested Successfully + + + You've successfully requested {numRequestedResources}{' '} + {pluralize(numRequestedResources, 'resource')} + + + + + + + ) : ( + + + + + {data.length} {pluralize(data.length, 'Resource')} Selected + + + + )} + {createAttempt.status === 'success' ? ( + + ) : ( + <> + {createAttempt.status === 'failed' && ( + + )} + ( + + { + clearAttempt(); + toggleResource( + resource.kind, + resource.id, + resource.name + ); + }} + disabled={createAttempt.status === 'processing'} + css={` + cursor: pointer; + + background-color: ${({ theme }) => + theme.colors.buttons.trashButton.default}; + border-radius: 2px; + + :hover { + background-color: ${({ theme }) => + theme.colors.buttons.trashButton.hover}; + } + `} + /> + + ), + }, + ]} + emptyText="No resources are selected" + /> + {userGroupFetchAttempt?.status === 'processing' && ( + + + + )} + {userGroupFetchAttempt?.status === 'failed' && ( + {userGroupFetchAttempt.statusText} + )} + {userGroupFetchAttempt?.status === 'success' && + appsGrantedByUserGroup.length > 0 && ( + + )} + {isResourceRequest && ( + + )} + + + + + {({ validator }) => ( + + {dryRunResponse && ( + + + + + )} + + {dryRunResponse && maxDuration && ( + + )} + + theme.colors.levels.sunken}; + `} + > + handleOnSubmit(validator)} + disabled={submitBtnDisabled} + > + Submit Request + + + + )} + + + )} +
+ )} +
+
+ ); +} + +function AppsGrantedAccess({ apps }: { apps: string[] }) { + const [expanded, setExpanded] = useState(true); + const ArrowIcon = expanded ? ChevronDown : ChevronRight; + + // if its a single app, just show the app they are getting access to + if (apps.length === 1) { + return ( + + + Grants access to the{' '} + + {apps[0]} + {' '} + app + + + ); + } + + return ( + + + setExpanded(!expanded)} + css={` + border-color: ${props => props.theme.colors.spotBackground[1]}; + `} + > + + + {`Grants access to ${apps.length} apps`} + + + {apps.length > 0 && } + + + {expanded && ( + + {apps.map(app => { + return {app}; + })} + + )} + + ); +} + +function ResourceRequestRoles({ + roles, + selectedRoles, + setSelectedRoles, + fetchAttempt, +}: { + roles: string[]; + selectedRoles: string[]; + setSelectedRoles: (roles: string[]) => void; + fetchAttempt: Attempt; +}) { + const [expanded, setExpanded] = useState(true); + const ArrowIcon = expanded ? ChevronDown : ChevronRight; + + function onInputChange( + roleName: string, + e: React.ChangeEvent + ) { + if (e.target.checked) { + return setSelectedRoles([...selectedRoles, roleName]); + } + setSelectedRoles(selectedRoles.filter(role => role !== roleName)); + } + + return ( + + + setExpanded(!expanded)} + css={` + border-color: ${props => props.theme.colors.spotBackground[1]}; + `} + > + + + Roles + + + {selectedRoles.length} role{selectedRoles.length !== 1 ? 's' : ''}{' '} + selected + + + {fetchAttempt.status === 'processing' ? ( + + + + ) : ( + + + + + + )} + + + {fetchAttempt.status === 'success' && expanded && ( + + {roles.map((roleName, index) => { + const id = `${roleName}${index}`; + return ( + theme.colors.levels.surface}; + + &:hover { + border-color: ${({ theme }) => + theme.colors.levels.elevated}; + } + `} + as="label" + htmlFor={id} + > + { + onInputChange(roleName, e); + }} + checked={selectedRoles.includes(roleName)} + /> + {roleName} + + ); + })} + {selectedRoles.length < roles.length && ( + theme.colors.levels.surface}; + `} + > + + + Modifying this role set may disable access to some of the above + resources. Use with caution. + + + )} + + )} + + ); +} + +function TextBox({ + reason, + updateReason, + requireReason, +}: { + reason: string; + updateReason(reason: string): void; + requireReason: boolean; +}) { + const { valid, message } = useRule(requireText(reason, requireReason)); + const hasError = !valid; + const labelText = hasError ? message : 'Request Reason'; + + const optionalText = requireReason ? '' : ' (optional)'; + const placeholder = `Describe your request...${optionalText}`; + + return ( + + {labelText} + updateReason(e.target.value)} + css={` + outline: none; + background: transparent; + + ::placeholder { + color: ${({ theme }) => theme.colors.text.muted}; + } + + &:hover, + &:focus, + &:active { + border: 1px solid ${props => props.theme.colors.text.slightlyMuted}; + } + `} + /> + + ); +} + +const requireText = (value: string, requireReason: boolean) => () => { + if (requireReason && (!value || value.trim().length === 0)) { + return { + valid: false, + message: 'Reason Required', + }; + } + return { valid: true }; +}; + +const SidePanel = styled(Box)` + position: absolute; + z-index: 11; + top: 0px; + right: 0px; + background: ${({ theme }) => theme.colors.levels.sunken}; + min-height: 100%; + width: 500px; + padding: 20px; + + &.entering { + right: -500px; + } + + &.entered { + right: 0px; + transition: right 300ms ease-out; + } + + &.exiting { + right: -500px; + transition: right 300ms ease-out; + } + + &.exited { + right: -500px; + } +`; + +const Dimmer = styled(Box)` + background: #000; + opacity: 0.5; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; +`; + +const StyledTable = styled(Table)` + & > tbody > tr > td { + vertical-align: middle; + } + + & > thead > tr > th { + background: ${props => props.theme.colors.spotBackground[1]}; + } + + border-radius: 8px; + box-shadow: ${props => props.theme.boxShadow[0]}; + overflow: hidden; +` as typeof Table; + +export type RequestCheckoutProps = { + onClose(): void; + toggleResource: ( + kind: ResourceKind, + resourceId: string, + resourceName?: string + ) => void; + appsGrantedByUserGroup?: string[]; + userGroupFetchAttempt?: Attempt; + reset: () => void; + SuccessComponent?: (params: SuccessComponentParams) => JSX.Element; + transitionState: TransitionStatus; + isResourceRequest: boolean; + requireReason: boolean; + selectedReviewers: ReviewerOption[]; + data: { kind: ResourceKind; name: string; id: string }[]; + setRequestTTL: (value: Option) => void; + createRequest: (req: CreateRequest) => void; + fetchStatus: 'loading' | 'loaded'; + fetchResourceRequestRolesAttempt: Attempt; + requestTTL: Option; + resourceRequestRoles: string[]; + reviewers: string[]; + setSelectedReviewers: (value: ReviewerOption[]) => void; + setMaxDuration: (value: Option) => void; + clearAttempt: () => void; + createAttempt: Attempt; + setSelectedResourceRequestRoles: (value: string[]) => void; + numRequestedResources: number; + selectedResourceRequestRoles: string[]; + dryRunResponse: AccessRequest; + maxDuration: Option; +}; + +type SuccessComponentParams = { + reset: () => void; + onClose: () => void; +}; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx new file mode 100644 index 0000000000000..5310bffb4cb63 --- /dev/null +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/SelectReviewers.tsx @@ -0,0 +1,373 @@ +import React, { useState, useRef } from 'react'; +import { components } from 'react-select'; +import ReactSelectCreatable from 'react-select/creatable'; +import styled from 'styled-components'; +import { ButtonBorder, Box, Text, Flex, ButtonIcon } from 'design'; +import * as Icon from 'design/Icon'; + +import { ReviewerOption } from './types'; + +export function SelectReviewers({ + reviewers, + selectedReviewers, + setSelectedReviewers, +}) { + const selectWrapperRef = useRef(null); + const reactSelectRef = useRef(null); + const [editReviewers, setEditReviewers] = useState(false); + const [suggestedReviewers, setSuggestedReviewers] = useState< + ReviewerOption[] + >( + // Initially, all suggested reviewers are selected for the requestor. + () => reviewers.map(r => ({ value: r, label: r, isDisabled: true })) + ); + + React.useEffect(() => { + // When editing reviewers, auto focus on input box. + if (editReviewers) { + reactSelectRef.current.focus(); + } + + // When editing reviewers, clicking outside box closes editor. + function handleOnClick(e) { + if (!editReviewers || e.target.closest('.react-select__option')) return; + + if (!selectWrapperRef.current?.contains(e.target)) { + setEditReviewers(false); + } + } + + window.addEventListener('click', handleOnClick); + + return () => { + window.removeEventListener('click', handleOnClick); + }; + }, [editReviewers]); + + const reviewerOptions = [ + { + label: '', + options: selectedReviewers, + }, + { + label: 'Suggested Reviewers', + options: suggestedReviewers, + }, + ]; + + // formatGroupLabel customizes react-select labels. + const formatGroupLabel = data => { + if (!data.label) { + return null; + } + return {data.label}; + }; + + // Option customizes how react-select options appear. + const Option = props => { + if (props.data.isDisabled) { + return null; + } + + if (props.data.isSelected) { + return ( + + + + + {props.data.value} + + + + + ); + } + + return ( + + + {props.data.label} + + + ); + }; + + function handleOnChange(values: ReviewerOption[]) { + const updateSelectedReviewers = values.map(r => ({ + value: r.value, + label: r.label, + // isSelected flag is used to customize style. + isSelected: true, + })); + + const updateSuggestedReviewers = suggestedReviewers.map(r => { + if (values.find(t => t.value === r.value)) { + // isDisabled flag is used to not render this name in suggested list. + r.isDisabled = true; + } else { + r.isDisabled = false; + } + return r; + }); + + setSelectedReviewers(updateSelectedReviewers); + setSuggestedReviewers(updateSuggestedReviewers); + } + + function toggleEditReviewers() { + setEditReviewers(!editReviewers); + } + + return ( + + + null} + ref={reactSelectRef} + /> + + + + ); +} + +function Reviewers({ + reviewers, + editReviewers, + toggleEditReviewers, + updateReviewers, +}: { + reviewers: ReviewerOption[]; + editReviewers: boolean; + toggleEditReviewers(): void; + updateReviewers(o: ReviewerOption[]): void; +}) { + const [expanded, setExpanded] = useState(true); + const ArrowIcon = expanded ? Icon.ChevronDown : Icon.ChevronRight; + + const $reviewers = reviewers.map((reviewer, index) => { + return ( + props.theme.colors.spotBackground[0]}; + `} + > + + {reviewer.value} + + + updateReviewers(reviewers.filter(r => r.value != reviewer.value)) + } + > + + + + ); + }); + + let btnTxt = 'Add'; + if (reviewers.length > 0) { + btnTxt = 'Edit'; + } + if (editReviewers) { + btnTxt = 'Done'; + } + + return ( + <> + props.theme.colors.spotBackground[1]}; + `} + > + + + Reviewers (optional) + + { + // By stopping propagation, + // we prevent this event from being interpreted as an outside click. + e.stopPropagation(); + toggleEditReviewers(); + }} + size="small" + width="50px" + > + {btnTxt} + + + {reviewers.length > 0 && ( + setExpanded(e => !e)}> + + + )} + + {expanded && {$reviewers}} + + ); +} + +const SelectWrapper = styled(Box)` + width: 260px; + height: 150px; + background-color: #ffffff; + color: #000000; + border-radius: 3px; + position: absolute; + z-index: 1; + top: 40px; + + .react-select__group, + .react-select__group-heading, + .react-select__menu-list { + padding: 0; + margin: 0; + } + + .react-select__menu-list { + margin-top: 10px; + } + + // Removes auto focus on first option + .react-select__option--is-focused { + background-color: inherit; + &:hover { + background-color: #deebff; + } + } + + .react-select-container { + width: 300px; + box-sizing: border-box; + border: none; + display: block; + font-size: 16px; + outline: none; + width: 100%; + background-color: #ffffff; + margin-top: 16px; + border-radius: 4px; + } + + .react-select__menu { + box-shadow: none; + } + + .react-select__control { + border-radius: 30px; + background-color: #f0f2f4; + margin: 0px 16px 10px 16px; + + &:hover { + cursor: pointer; + } + } + + .react-select__control--is-focused { + border-color: transparent; + box-shadow: none; + } + + .react-select__placeholder { + font-size: 14px; + } + + .react-select__option { + white-space: nowrap; + padding: 9px 16px; + border-top: 1px solid #eaeaea; + font-weight: bold; + font-size: 14px; + + &:hover { + cursor: pointer; + + &:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + } + } + + .icon-circlecheck { + color: transparent; + margin-right: 10px; + } + } + + .react-select__option--is-selected { + background-color: inherit; + color: inherit; + } + + .react-select__indicators { + display: none; + } + + .react-select__selected { + .icon-circlecheck { + color: ${props => props.theme.colors.success.main}; + } + + .icon-cross { + color: ${props => props.theme.colors.bgTerminal}; + display: none; + } + + &:hover .icon-cross { + display: block; + } + } +`; + +const SelectGroupLabel = styled(Box)` + width: 100%; + background-color: #efefef; + color: #324148; + text-transform: none; + padding: 3px 15px; +`; diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/__snapshots__/RequestCheckout.story.test.tsx.snap b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/__snapshots__/RequestCheckout.story.test.tsx.snap new file mode 100644 index 0000000000000..90726dbc9ad08 --- /dev/null +++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/__snapshots__/RequestCheckout.story.test.tsx.snap @@ -0,0 +1,3394 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`failed state 1`] = ` +.c8 { + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + box-sizing: border-box; + box-shadow: 0 1px 4px rgba(0,0,0,0.24); + margin: 0 0 24px 0; + min-height: 40px; + padding: 8px 16px; + overflow: auto; + word-break: break-word; + line-height: 1.5; + background: #FF6257; + color: #000000; +} + +.c8 a { + color: #FFFFFF; +} + +.c1 { + box-sizing: border-box; +} + +.c4 { + box-sizing: border-box; + margin-bottom: 16px; +} + +.c13 { + box-sizing: border-box; + margin-bottom: 4px; + margin-top: 40px; +} + +.c16 { + box-sizing: border-box; + margin-bottom: 8px; + padding-bottom: 8px; + height: 34px; + border-bottom: 1px solid; +} + +.c22 { + box-sizing: border-box; + margin-top: 40px; +} + +.c24 { + box-sizing: border-box; + margin-bottom: 4px; +} + +.c25 { + box-sizing: border-box; + margin-bottom: 8px; +} + +.c29 { + box-sizing: border-box; + max-width: 270px; + min-width: 200px; +} + +.c37 { + box-sizing: border-box; + padding: 8px; + height: 80px; + width: 100%; + color: #FFFFFF; + border: 1px solid; + border-radius: 4px; + border-color: rgba(255,255,255,0.54); +} + +.c39 { + box-sizing: border-box; + margin-bottom: 8px; + margin-top: 4px; + padding-bottom: 8px; + height: 34px; + border-bottom: 1px solid; +} + +.c42 { + box-sizing: border-box; + padding-top: 24px; + padding-bottom: 24px; +} + +.c21 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + color: #FFFFFF; + background: rgba(255,255,255,0); + border: 1px solid rgba(255,255,255,0.36); + font-size: 10px; + min-height: 24px; + padding: 0px 16px; + width: 50px; +} + +.c21:hover, +.c21:focus { + background: rgba(255,255,255,0.07); +} + +.c21:active { + background: rgba(255,255,255,0.13); +} + +.c21:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); + cursor: auto; +} + +.c44 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; + text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + color: #000000; + background: #9F85FF; + min-height: 40px; + font-size: 12px; + padding: 0px 40px; + width: 100%; +} + +.c44:hover, +.c44:focus { + background: #B29DFF; +} + +.c44:active { + background: #C5B6FF; +} + +.c44:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); + cursor: auto; +} + +.c41 { + align-items: center; + border: none; + cursor: pointer; + display: flex; + outline: none; + border-radius: 50%; + overflow: visible; + justify-content: center; + text-align: center; + flex: 0 0 auto; + background: transparent; + color: inherit; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + font-size: 16px; + height: 32px; + width: 32px; +} + +.c41:disabled { + color: rgba(255,255,255,0.36); + cursor: default; +} + +.c41:not(:disabled):hover, +.c41:not(:disabled):focus { + background: rgba(255,255,255,0.13); +} + +.c41:not(:disabled):active { + background: rgba(255,255,255,0.18); +} + +.c6 { + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 16px; +} + +.c31 { + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 16px; +} + +.c34 { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.c11 { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px; + border-radius: 4px; +} + +.c7 { + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + font-size: 18px; + line-height: 32px; + margin: 0px; + color: #FFFFFF; +} + +.c20 { + overflow: hidden; + text-overflow: ellipsis; + font-size: 12px; + margin: 0px; + margin-right: 8px; +} + +.c32 { + overflow: hidden; + text-overflow: ellipsis; + margin: 0px; + margin-right: 4px; +} + +.c28 { + color: #FFFFFF; + display: block; + font-size: 12px; + width: 100%; + margin-bottom: 4px; +} + +.c5 { + display: flex; + align-items: center; +} + +.c17 { + display: flex; + align-items: center; + justify-content: space-between; +} + +.c19 { + display: flex; +} + +.c23 { + display: flex; + flex-direction: column; + gap: 4px; +} + +.c26 { + display: flex; + align-items: end; + gap: 8px; +} + +.c9 { + border-collapse: collapse; + border-spacing: 0; + border-style: hidden; + font-size: 12px; + width: 100%; +} + +.c9 > thead > tr > th, +.c9 > tbody > tr > th, +.c9 > tfoot > tr > th, +.c9 > thead > tr > td, +.c9 > tbody > tr > td, +.c9 > tfoot > tr > td { + padding: 8px 8px; + vertical-align: middle; +} + +.c9 > thead > tr > th:first-child, +.c9 > tbody > tr > th:first-child, +.c9 > tfoot > tr > th:first-child, +.c9 > thead > tr > td:first-child, +.c9 > tbody > tr > td:first-child, +.c9 > tfoot > tr > td:first-child { + padding-left: 24px; +} + +.c9 > thead > tr > th:last-child, +.c9 > tbody > tr > th:last-child, +.c9 > tfoot > tr > th:last-child, +.c9 > thead > tr > td:last-child, +.c9 > tbody > tr > td:last-child, +.c9 > tfoot > tr > td:last-child { + padding-right: 24px; +} + +.c9 > tbody > tr > td { + vertical-align: middle; +} + +.c9 > thead > tr > th { + color: #FFFFFF; + font-weight: 600; + font-size: 14px; + line-height: 24px; + cursor: pointer; + padding-bottom: 0; + padding-top: 0; + text-align: left; + white-space: nowrap; +} + +.c9 > thead > tr > th svg { + height: 12px; +} + +.c9 > tbody > tr > td { + color: #FFFFFF; + font-weight: 300; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.035px; +} + +.c9 tbody tr { + transition: all 150ms; + position: relative; + border-top: 2px solid rgba(255,255,255,0.07); +} + +.c9 tbody tr:hover { + border-top: 2px solid rgba(0,0,0,0); + background-color: #222C59; +} + +.c9 tbody tr:hover:after { + box-shadow: 0px 1px 10px 0px rgba(0,0,0,0.12),0px 4px 5px 0px rgba(0,0,0,0.14),0px 2px 4px -1px rgba(0,0,0,0.20); + content: ''; + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 100%; + height: 100%; +} + +.c9 tbody tr:hover + tr { + border-top: 2px solid rgba(0,0,0,0); +} + +.c36 .react-select-container { + box-sizing: border-box; + display: block; + font-size: 14px; + outline: none; + width: 100%; + color: #FFFFFF; + background-color: transparent; + margin-bottom: 0px; + border-radius: 4px; +} + +.c36 .react-select__control { + outline: none; + min-height: 40px; + height: fit-content; + border: 1px solid rgba(255,255,255,0.54); + border-radius: 4px; + background-color: transparent; + box-shadow: none; +} + +.c36 .react-select__control .react-select__dropdown-indicator { + color: rgba(255,255,255,0.54); +} + +.c36 .react-select__control:hover, +.c36 .react-select__control:focus, +.c36 .react-select__control:active { + border: 1px solid rgba(255,255,255,0.72); + background-color: rgba(255,255,255,0.07); + cursor: pointer; +} + +.c36 .react-select__control:hover .react-select__dropdown-indicator, +.c36 .react-select__control:focus .react-select__dropdown-indicator, +.c36 .react-select__control:active .react-select__dropdown-indicator { + color: #FFFFFF; +} + +.c36 .react-select__control .react-select__indicator:hover, +.c36 .react-select__control .react-select__dropdown-indicator:hover, +.c36 .react-select__control .react-select__indicator:focus, +.c36 .react-select__control .react-select__dropdown-indicator:focus, +.c36 .react-select__control .react-select__indicator:active, +.c36 .react-select__control .react-select__dropdown-indicator:active { + color: #FFFFFF; +} + +.c36 .react-select__control--is-focused { + border-color: rgba(255,255,255,0.72); + background-color: rgba(255,255,255,0.07); + cursor: pointer; +} + +.c36 .react-select__control--is-focused .react-select__dropdown-indicator { + color: #FFFFFF; +} + +.c36 .react-select__single-value { + color: #FFFFFF; +} + +.c36 .react-select__placeholder { + color: rgba(255,255,255,0.54); +} + +.c36 .react-select__multi-value { + background-color: rgba(255,255,255,0.13); +} + +.c36 .react-select__multi-value .react-select__multi-value__label { + color: #FFFFFF; + padding: 0 6px; +} + +.c36 .react-select__multi-value .react-select__multi-value__remove { + color: #FFFFFF; +} + +.c36 .react-select__multi-value .react-select__multi-value__remove:hover { + background-color: rgba(255,255,255,0.07); + color: #FF6257; +} + +.c36 .react-select__option:hover { + cursor: pointer; + background-color: rgba(255,255,255,0.07); +} + +.c36 .react-select__option--is-focused { + background-color: rgba(255,255,255,0.07); +} + +.c36 .react-select__option--is-focused:hover { + cursor: pointer; + background-color: rgba(255,255,255,0.07); +} + +.c36 .react-select__option--is-selected { + background-color: rgba(255,255,255,0.13); + color: inherit; + font-weight: 500; +} + +.c36 .react-select__option--is-selected:hover { + background-color: rgba(255,255,255,0.13); +} + +.c36 .react-select__clear-indicator { + color: rgba(255,255,255,0.72); +} + +.c36 .react-select__clear-indicator:hover, +.c36 .react-select__clear-indicator:focus { + background-color: rgba(255,255,255,0.07); +} + +.c36 .react-select__clear-indicator:hover svg, +.c36 .react-select__clear-indicator:focus svg { + color: #FF6257; +} + +.c36 .react-select__menu { + margin-top: 0px; + background-color: #344179; + box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12); +} + +.c36 .react-select__menu .react-select__menu-list::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.13); + border-radius: 4px; +} + +.c36 .react-select__indicator-separator { + display: none; +} + +.c36 .react-select__loading-indicator { + display: none; +} + +.c36 .react-select__control--is-disabled { + color: rgba(255,255,255,0.36); + border: 1px solid rgba(255,255,255,0.36); +} + +.c36 .react-select__control--is-disabled .react-select__single-value, +.c36 .react-select__control--is-disabled .react-select__placeholder { + color: rgba(255,255,255,0.36); +} + +.c36 .react-select__control--is-disabled .react-select__indicator { + color: rgba(255,255,255,0.36); +} + +.c36 .react-select__input { + color: #FFFFFF; +} + +.c30 { + height: 40px; + border: 1px solid rgba(255,255,255,0.54); + border-radius: 4px; + padding: 0 8px; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.c30:hover { + background-color: rgba(255,255,255,0.07); + border: 1px solid rgba(255,255,255,0.72); +} + +.c27 { + position: relative; +} + +.c35 { + height: 18px; + width: 18px; + color: inherit; +} + +.c33 { + vertical-align: middle; + display: inline-block; + height: 18px; +} + +.c33:hover { + cursor: pointer; +} + +.c14 { + width: 260px; + height: 150px; + background-color: #ffffff; + color: #000000; + border-radius: 3px; + position: absolute; + z-index: 1; + top: 40px; +} + +.c14 .react-select__group, +.c14 .react-select__group-heading, +.c14 .react-select__menu-list { + padding: 0; + margin: 0; +} + +.c14 .react-select__menu-list { + margin-top: 10px; +} + +.c14 .react-select__option--is-focused { + background-color: inherit; +} + +.c14 .react-select__option--is-focused:hover { + background-color: #deebff; +} + +.c14 .react-select-container { + width: 300px; + box-sizing: border-box; + border: none; + display: block; + font-size: 16px; + outline: none; + width: 100%; + background-color: #ffffff; + margin-top: 16px; + border-radius: 4px; +} + +.c14 .react-select__menu { + box-shadow: none; +} + +.c14 .react-select__control { + border-radius: 30px; + background-color: #f0f2f4; + margin: 0px 16px 10px 16px; +} + +.c14 .react-select__control:hover { + cursor: pointer; +} + +.c14 .react-select__control--is-focused { + border-color: transparent; + box-shadow: none; +} + +.c14 .react-select__placeholder { + font-size: 14px; +} + +.c14 .react-select__option { + white-space: nowrap; + padding: 9px 16px; + border-top: 1px solid #eaeaea; + font-weight: bold; + font-size: 14px; +} + +.c14 .react-select__option:hover { + cursor: pointer; +} + +.c14 .react-select__option:hover:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.c14 .react-select__option .icon-circlecheck { + color: transparent; + margin-right: 10px; +} + +.c14 .react-select__option--is-selected { + background-color: inherit; + color: inherit; +} + +.c14 .react-select__indicators { + display: none; +} + +.c14 .react-select__selected .icon-circlecheck { + color: #00BFA6; +} + +.c14 .react-select__selected .icon-cross { + color: #010B1C; + display: none; +} + +.c14 .react-select__selected:hover .icon-cross { + display: block; +} + +.c15 { + width: 100%; + background-color: #efefef; + color: #324148; + text-transform: none; + padding: 3px 15px; +} + +.c18 { + border-color: rgba(255,255,255,0.13); +} + +.c40 { + border-color: rgba(255,255,255,0.13); +} + +.c3 { + position: absolute; + z-index: 11; + top: 0px; + right: 0px; + background: #0C143D; + min-height: 100%; + width: 500px; + padding: 20px; +} + +.c3.entering { + right: -500px; +} + +.c3.entered { + right: 0px; + transition: right 300ms ease-out; +} + +.c3.exiting { + right: -500px; + transition: right 300ms ease-out; +} + +.c3.exited { + right: -500px; +} + +.c2 { + background: #000; + opacity: 0.5; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; +} + +.c10 { + border-radius: 8px; + box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),0px 1px 1px rgba(0,0,0,0.14),0px 1px 3px rgba(0,0,0,0.12); + overflow: hidden; +} + +.c10 > tbody > tr > td { + vertical-align: middle; +} + +.c10 > thead > tr > th { + background: rgba(255,255,255,0.13); +} + +.c0 { + position: absolute; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + overflow: hidden; +} + +.c12 { + cursor: pointer; + background-color: rgba(255,255,255,0.07); + border-radius: 2px; +} + +.c12:hover { + background-color: rgba(255,255,255,0.13); +} + +.c43 { + position: sticky; + bottom: 0; + background: #0C143D; +} + +.c38 { + outline: none; + background: transparent; +} + +.c38::placeholder { + color: rgba(255,255,255,0.54); +} + +.c38:hover, +.c38:focus, +.c38:active { + border: 1px solid rgba(255,255,255,0.72); +} + +
+
+
+
+
+
+ + + + + + +
+
+ 5 + + Resources + Selected +
+
+
+
+ some error message +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Resource Kind + + Resource Name + +
+ app + + app-name + + + + + + + + +
+ db + + app-name + + + + + + + + +
+ kube_cluster + + kube-name + + + + + + + + +
+ user_group + + user-group-name + + + + + + + + +
+ windows_desktop + + desktop-name + + + + + + + + +
+
+
+