Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web: Add plugin status types (okta) and add error kind to ToolTIp.tsx #44788

Merged
merged 7 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion web/packages/design/src/DataTable/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function Table<T>({
style,
serversideProps,
customSort,
row,
}: State<T>) {
const renderHeaders = () => {
const headers = columns.flatMap(column => {
Expand Down Expand Up @@ -126,7 +127,15 @@ export function Table<T>({
</React.Fragment>
);
});
rows.push(<tr key={rowIdx}>{cells}</tr>);
rows.push(
<tr
key={rowIdx}
onClick={() => row?.onClick?.(item)}
style={row?.getStyle?.(item)}
>
{cells}
</tr>
);
});

if (rows.length) {
Expand Down
10 changes: 10 additions & 0 deletions web/packages/design/src/DataTable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ export type TableProps<T> = {
// any client table filtering supplied by default.
// Use case: filtering is done on the caller side e.g. server side.
disableFilter?: boolean;
/**
* row configuration
*/
row?: {
onClick?(row: T): void;
/**
* conditionally style a row (eg: cursor: pointer, disabled)
*/
getStyle?(row: T): React.CSSProperties;
};
};

type TableColumnBase<T> = {
Expand Down
1 change: 1 addition & 0 deletions web/packages/design/src/Modal/Modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export default class Modal extends React.Component {
data-testid="Modal"
ref={this.handleModalRef}
className={className}
onClick={e => e.stopPropagation()}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i couldn't see an issue with this, but the reason why i had to was because of this

image

the whole row had a click handler, and there is another click handler with options, if i didn't stop the modal propagation, once a user clicks on options and click away from it, it still triggered the rows click handler.

>
{!hideBackdrop && (
<Backdrop onClick={this.handleBackdropClick} {...BackdropProps} />
Expand Down
20 changes: 19 additions & 1 deletion web/packages/shared/components/ToolTip/ToolTip.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Text, Flex } from 'design';
import { ToolTipInfo } from './ToolTip';

export default {
title: 'Shared/ToolTip/Info',
title: 'Shared/ToolTip',
};

export const ShortContent = () => (
Expand Down Expand Up @@ -61,3 +61,21 @@ export const WithMutedIconColor = () => (
<ToolTipInfo muteIconColor>"some popover content"</ToolTipInfo>
</>
);

export const WithKindWarning = () => (
<>
<span css={{ marginRight: '4px', verticalAlign: 'middle' }}>
Hover the icon
</span>
<ToolTipInfo kind="warning">"some popover content"</ToolTipInfo>
</>
);

export const WithKindError = () => (
<>
<span css={{ marginRight: '4px', verticalAlign: 'middle' }}>
Hover the icon
</span>
<ToolTipInfo kind="error">"some popover content"</ToolTipInfo>
</>
);
14 changes: 13 additions & 1 deletion web/packages/shared/components/ToolTip/ToolTip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const ToolTipInfo: React.FC<
muteIconColor?: boolean;
sticky?: boolean;
maxWidth?: number;
kind?: 'info' | 'warning';
kind?: 'info' | 'warning' | 'error';
}>
> = ({
children,
Expand Down Expand Up @@ -79,6 +79,9 @@ export const ToolTipInfo: React.FC<
{kind === 'warning' && (
<WarningIcon $muteIconColor={muteIconColor} size="medium" />
)}
{kind === 'error' && (
<ErrorIcon $muteIconColor={muteIconColor} size="medium" />
)}
</span>
<Popover
modalCss={() =>
Expand Down Expand Up @@ -124,3 +127,12 @@ const WarningIcon = styled(Icons.Warning)<{ $muteIconColor?: boolean }>`
? p.theme.colors.text.disabled
: p.theme.colors.interactive.solid.alert.default.background};
`;

const ErrorIcon = styled(Icons.Warning)<{ $muteIconColor?: boolean }>`
height: 18px;
width: 18px;
color: ${p =>
p.$muteIconColor
? p.theme.colors.text.disabled
: p.theme.colors.interactive.solid.danger.default.background};
`;
26 changes: 26 additions & 0 deletions web/packages/teleport/src/Integrations/IntegrationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import React from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router';
import { Link as InternalRouteLink } from 'react-router-dom';

import { Box, Flex, Image } from 'design';
Expand Down Expand Up @@ -65,11 +66,27 @@ type Props<IntegrationLike> = {
type IntegrationLike = Integration | Plugin | ExternalAuditStorageIntegration;

export function IntegrationList(props: Props<IntegrationLike>) {
const history = useHistory();

function handleRowClick(row: IntegrationLike) {
if (row.kind !== 'okta') return;
history.push(cfg.getIntegrationStatusRoute(row.kind, row.name));
}

function getRowStyle(row: IntegrationLike): React.CSSProperties {
if (row.kind !== 'okta') return;
return { cursor: 'pointer' };
}

return (
<Table
pagination={{ pageSize: 20 }}
isSearchable
data={props.list}
row={{
onClick: handleRowClick,
getStyle: getRowStyle,
}}
columns={[
{
key: 'resourceType',
Expand Down Expand Up @@ -98,6 +115,15 @@ export function IntegrationList(props: Props<IntegrationLike>) {
return (
<Cell align="right">
<MenuButton>
{/* Currently, only okta supports status pages */}
{item.kind === 'okta' && (
<MenuItem
as={InternalRouteLink}
to={cfg.getIntegrationStatusRoute(item.kind, item.name)}
>
View Status
</MenuItem>
)}
<MenuItem onClick={() => props.onDeletePlugin(item)}>
Delete...
</MenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import React from 'react';
import { MemoryRouter } from 'react-router';

import {
IntegrationKind,
Expand All @@ -33,7 +34,11 @@ export default {
};

export function List() {
return <IntegrationList list={[...plugins, ...integrations]} />;
return (
<MemoryRouter>
<IntegrationList list={[...plugins, ...integrations]} />
</MemoryRouter>
);
}

export function DeleteDialog() {
Expand Down
7 changes: 6 additions & 1 deletion web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import type {
import type { SortType } from 'teleport/services/agents';
import type { RecordingType } from 'teleport/services/recordings';
import type { WebauthnAssertionResponse } from './services/auth';
import type { Regions } from './services/integrations';
import type { PluginKind, Regions } from './services/integrations';
import type { ParticipantMode } from 'teleport/services/session';
import type { YamlSupportedResourceKind } from './services/yaml/types';

Expand Down Expand Up @@ -180,6 +180,7 @@ const cfg = {
kubernetes: '/web/cluster/:clusterId/kubernetes',
headlessSso: `/web/headless/:requestId`,
integrations: '/web/integrations',
integrationStatus: '/web/integrations/status/:type/:name',
integrationEnroll: '/web/integrations/new/:type?',
locks: '/web/locks',
newLock: '/web/locks/new',
Expand Down Expand Up @@ -474,6 +475,10 @@ const cfg = {
return generatePath(cfg.routes.integrationEnroll, { type });
},

getIntegrationStatusRoute(type: PluginKind, name: string) {
return generatePath(cfg.routes.integrationStatus, { type, name });
},

getNodesRoute(clusterId: string) {
return generatePath(cfg.routes.nodes, { clusterId });
},
Expand Down
105 changes: 105 additions & 0 deletions web/packages/teleport/src/services/integrations/oktaStatusTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/**
* OktaPluginSyncStatusCode indicates the possible states of an Okta
* synchronization service.
*/
export enum PluginOktaSyncStatusCode {
/**
* is the status code zero value,
* indicating that the service has not yet reported a status code.
*/
Unspecified = 0,
/**
* indicates that the service is running without error
*/
Success = 1,
/**
* indicates that the service is currently in an error state.
*/
Error = 2,
}

export type PluginStatusOkta = {
orgUrl: string;
accessListSyncEnabled: boolean;
details?: PluginOktaDetails;
};

export type PluginOktaDetails = {
ssoDetails?: OktaSsoDetails;
appGroupSyncDetails?: OktaAppGroupSyncDetails;
usersSyncDetails?: OktaUserSyncDetails;
accessListsSyncDetails?: OktaAccessListSyncDetails;
scimDetails?: OktaScimDetails;
};

export type OktaSsoDetails = {
enabled: boolean;
appId: string;
appName: string;
};

export type OktaAppGroupSyncDetails = {
enabled: boolean;
statusCode: PluginOktaSyncStatusCode;
lastSuccess: Date;
lastFailed: Date;
numApps: number;
numGroups: number;
/**
* Error contains a textual description of the reason the last synchronization
* failed. Only valid when StatusCode is OKTA_PLUGIN_SYNC_STATUS_CODE_ERROR.
*/
error: string;
};

export type OktaUserSyncDetails = {
enabled: boolean;
statusCode: PluginOktaSyncStatusCode;
lastSuccess: Date;
lastFailed: Date;
numUsers: number;
/**
* Error contains a textual description of the reason the last synchronization
* failed. Only valid when StatusCode is OKTA_PLUGIN_SYNC_STATUS_CODE_ERROR.
*/
error: string;
};

export type OktaAccessListSyncDetails = {
enabled: boolean;
statusCode: PluginOktaSyncStatusCode;
lastSuccess: Date;
lastFailed: Date;
numApps: number;
numGroups: number;
appFilters: string[];
groupFilters: string[];
defaultOwners: string[];
/**
* Error contains a textual description of the reason the last synchronization
* failed. Only valid when StatusCode is OKTA_PLUGIN_SYNC_STATUS_CODE_ERROR.
*/
error: string;
};

export type OktaScimDetails = {
enabled: boolean;
};
7 changes: 7 additions & 0 deletions web/packages/teleport/src/services/integrations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ export type PluginKind =
| 'jamf'
| 'entra-id';

export type PluginStatus<S = any> = {
name: string;
type: PluginKind;
statusCode: IntegrationStatusCode;
stats?: S;
};

export type PluginOktaSpec = {
// scimBearerToken is the plain text of the bearer token that Okta will use
// to authenticate SCIM requests
Expand Down
Loading