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;