Skip to content
Open
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
26 changes: 20 additions & 6 deletions src/pages/permissions/CreateTlsIdentityBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,41 @@ import { useServerEntitlements } from "util/entitlements/server";

interface Props {
openPanel: () => void;
className?: string;
onClose?: () => void;
}

const CreateTlsIdentityBtn: FC<Props> = ({ openPanel }) => {
const CreateTlsIdentityBtn: FC<Props> = ({ openPanel, className, onClose }) => {
const isSmallScreen = useIsScreenBelow();
const { canCreateIdentities } = useServerEntitlements();

const handleClick = () => {
openPanel();
onClose?.();
};

const buttonClassName = className || "u-float-right u-no-margin--bottom";
const appearance = className?.includes("p-contextual-menu__link")
? "base"
: "positive";
const hasIcon =
!isSmallScreen && !className?.includes("p-contextual-menu__link");

return (
<>
<Button
appearance="positive"
className="u-float-right u-no-margin--bottom"
onClick={openPanel}
hasIcon={!isSmallScreen}
appearance={appearance}
className={buttonClassName}
onClick={handleClick}
hasIcon={hasIcon}
title={
canCreateIdentities()
? ""
: "You do not have permission to create identities"
}
disabled={!canCreateIdentities()}
>
{!isSmallScreen && <Icon name="plus" light />}
{hasIcon && <Icon name="plus" light />}
<span>Create TLS Identity</span>
</Button>
</>
Expand Down
48 changes: 48 additions & 0 deletions src/pages/permissions/OidcConfigurationBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { FC } from "react";
import OidcConfigurationModal from "./OidcConfigurationModal";
import { Button, usePortal } from "@canonical/react-components";
import { useServerEntitlements } from "util/entitlements/server";

interface Props {
isDisabled?: boolean;
className?: string;
onClose?: () => void;
}

const OidcConfigurationBtn: FC<Props> = ({
isDisabled,
className,
onClose,
}) => {
const { openPortal, closePortal, isOpen, Portal } = usePortal();
const { canEditServerConfiguration } = useServerEntitlements();

const handleClose = () => {
closePortal();
onClose?.();
};

return (
<>
{isOpen && (
<Portal>
<OidcConfigurationModal close={handleClose} />
</Portal>
)}
<Button
onClick={openPortal}
className={className}
disabled={isDisabled || !canEditServerConfiguration()}
title={
canEditServerConfiguration()
? ""
: "You do not have permission to edit server configuration"
}
>
Configure single sign-on
</Button>
</>
);
};

export default OidcConfigurationBtn;
109 changes: 109 additions & 0 deletions src/pages/permissions/OidcConfigurationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { FC } from "react";
import type { MainTableRow } from "@canonical/react-components/dist/components/MainTable/MainTable";
import {
MainTable,
Notification,
Spinner,
useNotify,
} from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { fetchConfigOptions } from "api/server";
import NotificationRow from "components/NotificationRow";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import { useClusteredSettings } from "context/useClusteredSettings";
import SsoNotification from "pages/permissions/SsoNotification";
import { getSettingRow } from "pages/settings/SettingsRow";
import { toConfigFields } from "util/config";
import { queryKeys } from "util/queryKeys";
import { getConfigFieldClusteredValue } from "util/settings";

interface Props {
closeModal: () => void;
}

const OidcConfigurationForm: FC<Props> = ({ closeModal }: Props) => {
const notify = useNotify();
const {
hasMetadataConfiguration,
settings,
isSettingsLoading,
settingsError,
} = useSupportedFeatures();
const { data: configOptions, isLoading: isConfigOptionsLoading } = useQuery({
queryKey: [queryKeys.configOptions],
queryFn: async () => fetchConfigOptions(hasMetadataConfiguration),
});
const {
data: clusteredSettings = [],
isLoading: isClusteredSettingsLoading,
error: clusterError,
} = useClusteredSettings();

if (clusterError) {
notify.failure("Loading clustered settings failed", clusterError);
closeModal();
return null;
}

if (settingsError) {
notify.failure("Loading settings failed", settingsError);
Comment thread
edlerd marked this conversation as resolved.
closeModal();
return null;
}

if (
isConfigOptionsLoading ||
isSettingsLoading ||
isClusteredSettingsLoading
) {
return <Spinner className="u-loader" text="Loading..." isMainComponent />;
}

const headers = [
{ content: "Group", className: "u-hide" },
{ content: "Key", className: "key" },
{ content: "Value" },
];
const configFields = toConfigFields(configOptions?.configs?.server ?? {});
const rows: MainTableRow[] = configFields
.filter((t) => t.key.startsWith("oidc"))
.map((configField) => {
const clusteredValue = getConfigFieldClusteredValue(
clusteredSettings,
configField,
);

return getSettingRow(
configField,
false,
clusteredValue,
() => {},
settings,
(message) => notify.success(message),
);
});

return (
<>
{!hasMetadataConfiguration && (
<Notification
severity="caution"
title="Get access to SSO configuration settings"
titleElement="h2"
>
Update to LXD v5.19.0 or later to access these settings
</Notification>
)}
<SsoNotification />
<NotificationRow className="u-no-padding" />
<MainTable
id="settings-table"
headers={headers}
rows={rows}
emptyStateMsg="No data to display"
/>
</>
);
};

export default OidcConfigurationForm;
28 changes: 28 additions & 0 deletions src/pages/permissions/OidcConfigurationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { FC, KeyboardEvent } from "react";
import { Modal } from "@canonical/react-components";
import OidcConfigurationForm from "pages/permissions/OidcConfigurationForm";

interface Props {
close: () => void;
}

const OidcConfigurationModal: FC<Props> = ({ close }) => {
Comment thread
edlerd marked this conversation as resolved.
const handleEscKey = (e: KeyboardEvent<HTMLElement>) => {
if (e.key === "Escape") {
close();
}
};

return (
<Modal
close={close}
className="edit-oidc-config settings"
Comment thread
edlerd marked this conversation as resolved.
title="Single sign-on configuration"
onKeyDown={handleEscKey}
>
<OidcConfigurationForm closeModal={close} />
</Modal>
);
};

export default OidcConfigurationModal;
10 changes: 3 additions & 7 deletions src/pages/permissions/PermissionIdentities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,14 @@ import BulkDeleteIdentitiesBtn from "./actions/BulkDeleteIdentitiesBtn";
import DeleteIdentityBtn from "./actions/DeleteIdentityBtn";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import { isUnrestricted } from "util/helpers";
import CreateTlsIdentityBtn from "./CreateTlsIdentityBtn";
import { useIdentities } from "context/useIdentities";
import { useIdentityEntitlements } from "util/entitlements/identities";
import { pluralize } from "util/helpers";
import { getIdentityName } from "util/permissionIdentities";
import CreateTLSIdentity from "./CreateTLSIdentity";
import CreateTLSIdentity from "pages/permissions/CreateTLSIdentity";
import PermissionIdentitiesActions from "pages/permissions/PermissionIdentitiesActions";
import ResourceLabel from "components/ResourceLabel";
import type { ResourceIconType } from "components/ResourceIcon";
import SsoNotification from "pages/permissions/SsoNotification";
import { AUTH_METHOD } from "util/authentication";

const PermissionIdentities: FC = () => {
const notify = useNotify();
Expand All @@ -53,7 +51,6 @@ const PermissionIdentities: FC = () => {
const [selectedIdentityIds, setSelectedIdentityIds] = useState<string[]>([]);
const { hasAccessManagementTLS } = useSupportedFeatures();
const { canEditIdentity } = useIdentityEntitlements();
const hasOidc = settings?.auth_methods?.includes(AUTH_METHOD.OIDC);

useEffect(() => {
const validIdentityIds = new Set(identities.map((identity) => identity.id));
Expand Down Expand Up @@ -312,7 +309,7 @@ const PermissionIdentities: FC = () => {
)}
</PageHeader.Left>
<PageHeader.BaseActions>
<CreateTlsIdentityBtn
<PermissionIdentitiesActions
openPanel={panelParams.openCreateTLSIdentity}
/>
</PageHeader.BaseActions>
Expand All @@ -322,7 +319,6 @@ const PermissionIdentities: FC = () => {
{!panelParams.panel && (
<div className="row">
<NotificationConsumer />
<SsoNotification hasOidc={hasOidc ?? false} />
</div>
)}
<Row>
Expand Down
58 changes: 58 additions & 0 deletions src/pages/permissions/PermissionIdentitiesActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { FC } from "react";
import { cloneElement } from "react";
import { ContextualMenu } from "@canonical/react-components";
import {
largeScreenBreakpoint,
useIsScreenBelow,
} from "context/useIsScreenBelow";
import OidcConfigurationBtn from "pages/permissions/OidcConfigurationBtn";
import CreateTlsIdentityBtn from "pages/permissions/CreateTlsIdentityBtn";

interface Props {
openPanel: () => void;
}

const PermissionIdentitiesActions: FC<Props> = ({ openPanel }) => {
const isSmallScreen = useIsScreenBelow(largeScreenBreakpoint);

const classname = isSmallScreen
? "p-contextual-menu__link"
: "p-segmented-control__button";

const menuElements = [
<OidcConfigurationBtn key="oidc" className={classname} />,
<CreateTlsIdentityBtn
key="create"
openPanel={openPanel}
className={classname}
/>,
];

return (
<>
{isSmallScreen ? (
<ContextualMenu
closeOnOutsideClick={false}
toggleLabel="Actions"
position="left"
hasToggleIcon
title="actions"
>
{(close: () => void) => (
<span>
{[...menuElements].map((item) =>
cloneElement(item, { onClose: close }),
)}
</span>
)}
</ContextualMenu>
) : (
<div className="p-segmented-control">
<div className="p-segmented-control__list">{menuElements}</div>
</div>
)}
</>
);
};

export default PermissionIdentitiesActions;
Loading
Loading