github.com/minio/console@v1.4.1/web-app/src/screens/Console/IDP/IDPConfigurationDetails.tsx (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2022 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  import React, { Fragment, useCallback, useEffect, useState } from "react";
    18  import {
    19    BackLink,
    20    Box,
    21    breakPoints,
    22    Button,
    23    ConsoleIcon,
    24    EditIcon,
    25    FormLayout,
    26    Grid,
    27    HelpBox,
    28    InputBox,
    29    PageLayout,
    30    RefreshIcon,
    31    ScreenTitle,
    32    Switch,
    33    Tooltip,
    34    TrashIcon,
    35    ValuePair,
    36    WarnIcon,
    37  } from "mds";
    38  import { useNavigate, useParams } from "react-router-dom";
    39  import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
    40  import { useAppDispatch } from "../../../store";
    41  import {
    42    setErrorSnackMessage,
    43    setHelpName,
    44    setServerNeedsRestart,
    45  } from "../../../systemSlice";
    46  import DeleteIDPConfigurationModal from "./DeleteIDPConfigurationModal";
    47  import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
    48  import HelpMenu from "../HelpMenu";
    49  import { api } from "api";
    50  import {
    51    ApiError,
    52    HttpResponse,
    53    IdpServerConfiguration,
    54    SetIDPResponse,
    55  } from "api/consoleApi";
    56  import { errorToHandler } from "api/errors";
    57  
    58  type IDPConfigurationDetailsProps = {
    59    formFields: object;
    60    endpoint: string;
    61    backLink: string;
    62    header: string;
    63    idpType: string;
    64    helpBox: React.ReactNode;
    65    icon: React.ReactNode;
    66  };
    67  
    68  const IDPConfigurationDetails = ({
    69    formFields,
    70    endpoint,
    71    backLink,
    72    header,
    73    idpType,
    74    icon,
    75    helpBox,
    76  }: IDPConfigurationDetailsProps) => {
    77    const dispatch = useAppDispatch();
    78    const navigate = useNavigate();
    79    const params = useParams();
    80  
    81    const configurationName = params.idpName;
    82  
    83    const [loadingDetails, setLoadingDetails] = useState<boolean>(true);
    84    const [loadingSave, setLoadingSave] = useState<boolean>(false);
    85    const [loadingEnabledSave, setLoadingEnabledSave] = useState<boolean>(false);
    86    const [isEnabled, setIsEnabled] = useState<boolean>(false);
    87    const [fields, setFields] = useState<any>({});
    88    const [overrideFields, setOverrideFields] = useState<any>({});
    89    const [originalFields, setOriginalFields] = useState<any>({});
    90    const [record, setRecord] = useState<any>({});
    91    const [editMode, setEditMode] = useState<boolean>(false);
    92    const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
    93    const [envOverride, setEnvOverride] = useState<boolean>(false);
    94  
    95    const parseFields = useCallback(
    96      (record: any) => {
    97        let fields: any = {};
    98        let overrideFields: any = {};
    99        let totEnv = 0;
   100  
   101        if (record.info) {
   102          record.info.forEach((item: any) => {
   103            if (item.key === "enable") {
   104              setIsEnabled(item.value === "on");
   105            }
   106  
   107            if (item.isEnv) {
   108              overrideFields[item.key] =
   109                `MINIO_IDENTITY_OPENID_${item.key.toUpperCase()}${
   110                  configurationName !== "_" ? `_${configurationName}` : ""
   111                }`;
   112              totEnv++;
   113            }
   114  
   115            fields[item.key] = item.value;
   116          });
   117  
   118          if (totEnv > 0) {
   119            setEnvOverride(true);
   120          }
   121        }
   122        setFields(fields);
   123        setOverrideFields(overrideFields);
   124      },
   125      [configurationName],
   126    );
   127  
   128    const toggleEditMode = () => {
   129      if (editMode) {
   130        parseFields(record);
   131      }
   132      setEditMode(!editMode);
   133    };
   134  
   135    const parseOriginalFields = (record: any) => {
   136      let fields: any = {};
   137      if (record.info) {
   138        record.info.forEach((item: any) => {
   139          fields[item.key] = item.value;
   140        });
   141      }
   142      setOriginalFields(fields);
   143    };
   144  
   145    useEffect(() => {
   146      const loadRecord = () => {
   147        api.idp
   148          .getConfiguration(configurationName || "", "openid")
   149          .then((res: HttpResponse<IdpServerConfiguration, ApiError>) => {
   150            if (res.data) {
   151              setRecord(res.data);
   152              parseFields(res.data);
   153              parseOriginalFields(res.data);
   154            }
   155          })
   156          .catch((res: HttpResponse<IdpServerConfiguration, ApiError>) => {
   157            dispatch(setErrorSnackMessage(errorToHandler(res.error)));
   158          })
   159          .finally(() => setLoadingDetails(false));
   160      };
   161  
   162      if (loadingDetails) {
   163        loadRecord();
   164      }
   165    }, [dispatch, loadingDetails, configurationName, endpoint, parseFields]);
   166  
   167    const validSave = () => {
   168      for (const [key, value] of Object.entries(formFields)) {
   169        if (
   170          value.required &&
   171          !(
   172            fields[key] !== undefined &&
   173            fields[key] !== null &&
   174            fields[key] !== ""
   175          )
   176        ) {
   177          return false;
   178        }
   179      }
   180      return true;
   181    };
   182  
   183    const resetForm = () => {
   184      setFields({});
   185    };
   186  
   187    const saveRecord = (event: React.FormEvent) => {
   188      setLoadingSave(true);
   189      event.preventDefault();
   190      let input = "";
   191      for (const key of Object.keys(formFields)) {
   192        if (fields[key] || fields[key] !== originalFields[key]) {
   193          input += `${key}=${fields[key]} `;
   194        }
   195      }
   196  
   197      api.idp
   198        .updateConfiguration(configurationName || "", "openid", { input })
   199        .then((res: HttpResponse<SetIDPResponse, ApiError>) => {
   200          if (res.data) {
   201            dispatch(setServerNeedsRestart(res.data.restart === true));
   202            setEditMode(false);
   203          }
   204        })
   205        .catch(async (res: HttpResponse<SetIDPResponse, ApiError>) => {
   206          dispatch(setErrorSnackMessage(errorToHandler(res.error)));
   207        })
   208        .finally(() => setLoadingSave(false));
   209    };
   210  
   211    const closeDeleteModalAndRefresh = async (refresh: boolean) => {
   212      setDeleteOpen(false);
   213  
   214      if (refresh) {
   215        navigate(backLink);
   216      }
   217    };
   218  
   219    const toggleConfiguration = (value: boolean) => {
   220      setLoadingEnabledSave(true);
   221      const input = `enable=${value ? "on" : "off"}`;
   222  
   223      api.idp
   224        .updateConfiguration(configurationName || "", "openid", { input: input })
   225        .then((res: HttpResponse<SetIDPResponse, ApiError>) => {
   226          if (res.data) {
   227            setIsEnabled(!isEnabled);
   228            dispatch(setServerNeedsRestart(res.data.restart === true));
   229          }
   230        })
   231        .catch((res: HttpResponse<SetIDPResponse, ApiError>) => {
   232          dispatch(setErrorSnackMessage(errorToHandler(res.error)));
   233        })
   234        .finally(() => setLoadingEnabledSave(false));
   235    };
   236  
   237    const renderFormField = (key: string, value: any) => {
   238      switch (value.type) {
   239        case "toggle":
   240          return (
   241            <Switch
   242              indicatorLabels={["Enabled", "Disabled"]}
   243              checked={fields[key] === "on"}
   244              value={"is-field-enabled"}
   245              id={"is-field-enabled"}
   246              name={"is-field-enabled"}
   247              label={value.label}
   248              tooltip={value.tooltip}
   249              onChange={(e) =>
   250                setFields({ ...fields, [key]: e.target.checked ? "on" : "off" })
   251              }
   252              description=""
   253              disabled={!editMode}
   254            />
   255          );
   256        default:
   257          return (
   258            <InputBox
   259              id={key}
   260              required={value.required}
   261              name={key}
   262              label={value.label}
   263              tooltip={value.tooltip}
   264              error={value.hasError(fields[key], editMode)}
   265              value={fields[key] ? fields[key] : ""}
   266              onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
   267                setFields({ ...fields, [key]: e.target.value })
   268              }
   269              placeholder={value.placeholder}
   270              disabled={!editMode}
   271              type={value.type}
   272            />
   273          );
   274      }
   275    };
   276  
   277    const renderEditForm = () => {
   278      return (
   279        <FormLayout helpBox={helpBox}>
   280          <form
   281            noValidate
   282            autoComplete="off"
   283            onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   284              saveRecord(e);
   285            }}
   286          >
   287            <Grid container>
   288              {editMode ? (
   289                <Grid item xs={12} sx={{ marginBottom: 15 }}>
   290                  <HelpBox
   291                    title={
   292                      <Box
   293                        style={{
   294                          display: "flex",
   295                          justifyContent: "space-between",
   296                          alignItems: "center",
   297                          flexGrow: 1,
   298                        }}
   299                      >
   300                        Client Secret must be re-entered to change OpenID
   301                        configurations
   302                      </Box>
   303                    }
   304                    iconComponent={<WarnIcon />}
   305                    help={null}
   306                  />
   307                </Grid>
   308              ) : null}
   309              <Grid xs={12} item>
   310                {Object.entries(formFields).map(([key, value]) =>
   311                  renderFormField(key, value),
   312                )}
   313                <Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
   314                  {editMode && (
   315                    <Button
   316                      id={"clear"}
   317                      type="button"
   318                      variant="regular"
   319                      onClick={resetForm}
   320                      label={"Clear"}
   321                    />
   322                  )}
   323                  {editMode && (
   324                    <Button
   325                      id={"cancel"}
   326                      type="button"
   327                      variant="regular"
   328                      onClick={toggleEditMode}
   329                      label={"Cancel"}
   330                    />
   331                  )}
   332                  {editMode && (
   333                    <Button
   334                      id={"save-key"}
   335                      type="submit"
   336                      variant="callAction"
   337                      color="primary"
   338                      disabled={loadingDetails || loadingSave || !validSave()}
   339                      label={"Save"}
   340                    />
   341                  )}
   342                </Grid>
   343              </Grid>
   344            </Grid>
   345          </form>
   346        </FormLayout>
   347      );
   348    };
   349    const renderViewForm = () => {
   350      return (
   351        <Box
   352          withBorders
   353          sx={{
   354            display: "grid",
   355            gridTemplateColumns: "1fr",
   356            gridAutoFlow: "dense",
   357            gap: 3,
   358            padding: "15px",
   359            [`@media (min-width: ${breakPoints.sm}px)`]: {
   360              gridTemplateColumns: "2fr 1fr",
   361              gridAutoFlow: "row",
   362            },
   363          }}
   364        >
   365          {Object.entries(formFields).map(([key, value]) => {
   366            if (!value.editOnly) {
   367              let label: React.ReactNode = value.label;
   368              let val: React.ReactNode = fields[key] ? fields[key] : "";
   369  
   370              if (value.type === "toggle" && fields[key]) {
   371                if (val !== "on") {
   372                  val = "Off";
   373                } else {
   374                  val = "On";
   375                }
   376              }
   377  
   378              if (overrideFields[key]) {
   379                label = (
   380                  <Box
   381                    sx={{
   382                      display: "flex",
   383                      alignItems: "center",
   384                      gap: 5,
   385                      "& .min-icon": {
   386                        height: 20,
   387                        width: 20,
   388                      },
   389                      "& span": {
   390                        height: 20,
   391                        display: "flex",
   392                        alignItems: "center",
   393                      },
   394                    }}
   395                  >
   396                    <span>{value.label}</span>
   397                    <Tooltip
   398                      tooltip={`This value is set from the ${overrideFields[key]} environment variable`}
   399                      placement={"right"}
   400                    >
   401                      <span className={"muted"}>
   402                        <ConsoleIcon />
   403                      </span>
   404                    </Tooltip>
   405                  </Box>
   406                );
   407  
   408                val = (
   409                  <i>
   410                    <span className={"muted"}>{val}</span>
   411                  </i>
   412                );
   413              }
   414              return <ValuePair key={key} label={label} value={val} />;
   415            }
   416            return null;
   417          })}
   418        </Box>
   419      );
   420    };
   421  
   422    useEffect(() => {
   423      dispatch(setHelpName("idp_config"));
   424    }, [dispatch]);
   425  
   426    return (
   427      <Fragment>
   428        {deleteOpen && configurationName && (
   429          <DeleteIDPConfigurationModal
   430            deleteOpen={deleteOpen}
   431            idp={configurationName}
   432            idpType={idpType}
   433            closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
   434          />
   435        )}
   436        <Grid item xs={12}>
   437          <PageHeaderWrapper
   438            label={<BackLink onClick={() => navigate(backLink)} label={header} />}
   439            actions={<HelpMenu />}
   440          />
   441          <PageLayout>
   442            <ScreenTitle
   443              icon={icon}
   444              title={
   445                configurationName === "_" ? "Default" : configurationName || ""
   446              }
   447              subTitle={null}
   448              actions={
   449                <Fragment>
   450                  {configurationName !== "_" && (
   451                    <Tooltip
   452                      tooltip={
   453                        envOverride
   454                          ? "This configuration cannot be deleted using this module as this was set using OpenID environment variables."
   455                          : ""
   456                      }
   457                    >
   458                      <Button
   459                        id={"delete-idp-config"}
   460                        onClick={() => {
   461                          setDeleteOpen(true);
   462                        }}
   463                        label={"Delete Configuration"}
   464                        icon={<TrashIcon />}
   465                        variant={"secondary"}
   466                        disabled={envOverride}
   467                      />
   468                    </Tooltip>
   469                  )}
   470                  {!editMode && (
   471                    <Tooltip
   472                      tooltip={
   473                        envOverride
   474                          ? "Configuration cannot be edited in this module as OpenID environment variables are set for this MinIO instance."
   475                          : ""
   476                      }
   477                    >
   478                      <Button
   479                        id={"edit"}
   480                        type="button"
   481                        variant={"callAction"}
   482                        icon={<EditIcon />}
   483                        onClick={toggleEditMode}
   484                        label={"Edit"}
   485                        disabled={envOverride}
   486                      />
   487                    </Tooltip>
   488                  )}
   489                  <Tooltip
   490                    tooltip={
   491                      envOverride
   492                        ? "Configuration cannot be disabled / enabled in this module as OpenID environment variables are set for this MinIO instance."
   493                        : ""
   494                    }
   495                  >
   496                    <Button
   497                      id={"is-configuration-enabled"}
   498                      onClick={() => toggleConfiguration(!isEnabled)}
   499                      label={isEnabled ? "Disable" : "Enable"}
   500                      disabled={loadingEnabledSave || envOverride}
   501                    />
   502                  </Tooltip>
   503                  <Button
   504                    id={"refresh-idp-config"}
   505                    onClick={() => setLoadingDetails(true)}
   506                    label={"Refresh"}
   507                    icon={<RefreshIcon />}
   508                  />
   509                </Fragment>
   510              }
   511              sx={{
   512                marginBottom: 15,
   513              }}
   514            />
   515            {editMode ? renderEditForm() : renderViewForm()}
   516          </PageLayout>
   517        </Grid>
   518      </Fragment>
   519    );
   520  };
   521  
   522  export default IDPConfigurationDetails;