2020-05-20 02:54:25 -08:00
/ *
* Copyright ( C ) 2020 Red Hat , Inc .
2026-02-01 03:22:50 -09:00
* SPDX - License - Identifier : LGPL - 2.1 - or - later
2020-05-20 02:54:25 -08:00
* /
2025-08-13 06:55:53 -08:00
import React , { useState , useEffect , useRef , useMemo } from 'react' ;
2020-05-20 02:54:25 -08:00
2023-02-22 10:29:46 -09:00
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js" ;
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js" ;
2023-04-24 06:27:34 -08:00
import { Card , CardBody , CardHeader , CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js' ;
2025-03-21 04:00:37 -08:00
import { EmptyState , EmptyStateActions , EmptyStateFooter , EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js" ;
2023-05-10 00:32:32 -08:00
import { Flex , FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js" ;
2023-02-22 10:29:46 -09:00
import { HelperText , HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js" ;
2023-05-15 23:31:19 -08:00
import { Label , LabelGroup } from "@patternfly/react-core/dist/esm/components/Label/index.js" ;
2023-04-21 04:39:09 -08:00
import { Page , PageBreadcrumb , PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js" ;
2023-02-22 10:29:46 -09:00
import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js" ;
import { Breadcrumb , BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js" ;
import { Form , FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js" ;
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js" ;
import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js" ;
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js" ;
2022-12-12 06:49:04 -09:00
import { ExclamationCircleIcon , HelpIcon , UndoIcon } from '@patternfly/react-icons' ;
2023-12-15 01:12:51 -09:00
import cockpit from 'cockpit' ;
import { superuser } from "superuser" ;
2024-06-24 11:59:51 -08:00
import * as timeformat from "timeformat" ;
2023-12-15 01:12:51 -09:00
import { apply _modal _dialog } from "cockpit-components-dialog.jsx" ;
2024-11-28 03:00:06 -09:00
import { MultiTypeaheadSelect } from "cockpit-components-multi-typeahead-select" ;
2023-12-15 01:12:51 -09:00
2026-04-29 10:54:42 -08:00
import { FormHelper } from "cockpit-components-form-helper" ;
2020-05-20 02:54:25 -08:00
import { show _unexpected _error } from "./dialog-utils.js" ;
import { delete _account _dialog } from "./delete-account-dialog.js" ;
import { account _expiration _dialog , password _expiration _dialog } from "./expiration-dialogs.js" ;
2023-07-10 07:16:48 -08:00
import { account _shell _dialog } from "./shell-dialog.js" ;
2020-05-20 02:54:25 -08:00
import { set _password _dialog , reset _password _dialog } from "./password-dialogs.js" ;
2021-09-24 07:11:06 -08:00
import { AccountLogs } from "./account-logs-panel.jsx" ;
2020-05-20 02:54:25 -08:00
import { AuthorizedKeys } from "./authorized-keys-panel.js" ;
2024-05-23 04:48:39 -08:00
import { get _locked } from "./utils.js" ;
2020-05-20 02:54:25 -08:00
const _ = cockpit . gettext ;
function get _expire ( name ) {
function parse _expire ( data ) {
let account _expiration = '' ;
let account _date = null ;
let password _expiration = '' ;
let password _days = null ;
data . split ( '\n' ) . forEach ( line => {
const fields = line . split ( ': ' ) ;
if ( fields [ 0 ] && fields [ 0 ] . indexOf ( "Password expires" ) === 0 ) {
if ( fields [ 1 ] . indexOf ( "never" ) === 0 ) {
password _expiration = _ ( "Never expire password" ) ;
} else if ( fields [ 1 ] . indexOf ( "password must be changed" ) === 0 ) {
password _expiration = _ ( "Password must be changed" ) ;
} else {
2021-06-29 22:02:02 -08:00
password _expiration = cockpit . format ( _ ( "Require password change on $0" ) , timeformat . date ( new Date ( fields [ 1 ] ) ) ) ;
2020-05-20 02:54:25 -08:00
}
} else if ( fields [ 0 ] && fields [ 0 ] . indexOf ( "Account expires" ) === 0 ) {
if ( fields [ 1 ] . indexOf ( "never" ) === 0 ) {
2021-05-11 10:56:57 -08:00
account _expiration = _ ( "Never expire account" ) ;
2020-05-20 02:54:25 -08:00
} else {
account _date = new Date ( fields [ 1 ] + " 12:00:00 UTC" ) ;
2021-06-29 22:02:02 -08:00
account _expiration = cockpit . format ( _ ( "Expire account on $0" ) , timeformat . date ( new Date ( fields [ 1 ] ) ) ) ;
2020-05-20 02:54:25 -08:00
}
} else if ( fields [ 0 ] && fields [ 0 ] . indexOf ( "Maximum number of days between password change" ) === 0 ) {
password _days = fields [ 1 ] ;
}
} ) ;
2026-04-22 02:41:15 -08:00
// 99999 traditionally meant "never" but the modern value for
// that is -1.
//
// Modern versions of "chage" will not treat 99999 any special
// and will output bogus expiration dates somewhere in 2300.
// But let's recognize both, since old accounts will still use
// 99999, and /etc/login.def also still uses 99999 as the
// default for new accounts.
//
2026-08-05 02:09:06 -08:00
// Password expiration is being removed completely from
2026-04-22 02:41:15 -08:00
// shadow-utils, and we probably should continue to recognize
// 99999 as "never" until we stop supporting password expiry
// entirely.
if ( parseInt ( password _days ) >= 99999 || parseInt ( password _days ) < 0 ) {
password _days = null ;
password _expiration = _ ( "Never expire password" ) ;
}
2020-05-20 02:54:25 -08:00
return {
account _text : account _expiration ,
2023-02-27 07:42:41 -09:00
account _date ,
2020-05-20 02:54:25 -08:00
password _text : password _expiration ,
2023-02-27 07:42:41 -09:00
password _days
2020-05-20 02:54:25 -08:00
} ;
}
2023-02-28 22:44:05 -09:00
return cockpit . spawn ( [ "chage" , "-l" , name ] ,
2020-05-20 02:54:25 -08:00
{ environ : [ "LC_ALL=C" ] , err : "message" , superuser : "try" } )
. catch ( ( ) => "" )
. then ( parse _expire ) ;
}
2025-08-13 06:55:53 -08:00
export function AccountNotFound ( ) {
return (
< EmptyState headingLevel = "h1" icon = { ExclamationCircleIcon } titleText = { _ ( "Account not available or cannot be edited." ) } variant = { EmptyStateVariant . sm } id = "account-failure" >
< EmptyStateFooter >
< EmptyStateActions >
< Breadcrumb >
< BreadcrumbItem to = "#/" > { _ ( "Back to accounts" ) } < / B r e a d c r u m b I t e m >
< / B r e a d c r u m b >
< / E m p t y S t a t e A c t i o n s >
< / E m p t y S t a t e F o o t e r >
< / E m p t y S t a t e >
) ;
}
export function AccountDetails ( { account , groups , isLoading , current _user , shells } ) {
2022-11-08 00:55:11 -09:00
const [ expiration , setExpiration ] = useState ( null ) ;
2020-11-23 02:10:50 -09:00
2025-08-13 06:55:53 -08:00
const user = useMemo ( ( ) => account . name , [ account . name ] ) ;
const [ isLocked , setIsLocked ] = useState ( account . isLocked ) ;
2020-05-20 02:54:25 -08:00
2025-07-04 02:49:05 -08:00
const [ editedRealName , setEditedRealName ] = useState ( null ) ;
2026-04-29 10:54:42 -08:00
const [ realNameError , setRealNameError ] = useState ( "" ) ;
2026-10-14 04:02:30 -08:00
const [ committingRealName , setCommittingRealName ] = useState ( false ) ;
2025-08-13 06:55:53 -08:00
const [ disableLockedEdit , setDisableLockedEdit ] = useState ( false ) ;
useEffect ( ( ) => {
get _expire ( account . name ) . then ( setExpiration ) ;
} , [ account ] ) ;
2020-05-20 02:54:25 -08:00
2025-08-13 06:55:53 -08:00
// Only update isLocked field if account changes value
useEffect ( ( ) => {
setIsLocked ( account . isLocked ) ;
} , [ account . isLocked ] ) ;
2020-05-20 02:54:25 -08:00
2025-07-04 02:49:05 -08:00
function changeRealName ( ) {
2026-04-29 10:54:42 -08:00
setRealNameError ( "" ) ;
2025-07-04 02:49:05 -08:00
if ( editedRealName === null || editedRealName === undefined )
2020-05-20 02:54:25 -08:00
return ;
2026-04-29 10:54:42 -08:00
if ( editedRealName . includes ( ':' ) ) {
setRealNameError ( _ ( "The full name must not contain colons." ) ) ;
return ;
}
2025-07-04 02:49:05 -08:00
setCommittingRealName ( true ) ;
2020-05-20 02:54:25 -08:00
2025-07-04 02:49:05 -08:00
cockpit . spawn ( [ "/usr/sbin/usermod" , user , "--comment" , editedRealName ] ,
2020-05-20 02:54:25 -08:00
{ superuser : "try" , err : "message" } )
. then ( ( ) => {
2025-07-04 02:49:05 -08:00
setEditedRealName ( null ) ;
setCommittingRealName ( false ) ;
2020-05-20 02:54:25 -08:00
} )
. catch ( error => {
2025-07-04 02:49:05 -08:00
setEditedRealName ( null ) ;
setCommittingRealName ( false ) ;
2020-05-20 02:54:25 -08:00
show _unexpected _error ( error ) ;
} ) ;
}
function change _locked ( value , dont _retry _if _stuck ) {
2025-08-13 06:55:53 -08:00
setIsLocked ( value ) ;
setDisableLockedEdit ( true ) ;
2020-05-20 02:54:25 -08:00
cockpit . spawn ( [ "/usr/sbin/usermod" , user , value ? "--lock" : "--unlock" ] ,
{ superuser : "require" , err : "message" } )
. then ( ( ) => {
get _locked ( user )
. then ( locked => {
/ * i f w e c a r e a b o u t w h a t t h e l o c k s t a t e s h o u l d b e a n d i t d o e s n ' t m a t c h , t r y t o c h a n g e a g a i n
this is a workaround for different ways of handling a locked account
https : //github.com/cockpit-project/cockpit/issues/1216
https : //bugzilla.redhat.com/show_bug.cgi?id=853153
* /
if ( locked != value && ! dont _retry _if _stuck ) {
console . log ( "Account locked state doesn't match desired value, trying again." ) ;
// only retry once to avoid uncontrolled recursion
change _locked ( value , true ) ;
2025-08-13 06:55:53 -08:00
}
2020-05-20 02:54:25 -08:00
} ) ;
} )
. catch ( error => {
show _unexpected _error ( error ) ;
2025-08-13 06:55:53 -08:00
setIsLocked ( ! value ) ;
} )
. finally ( ( ) => {
setDisableLockedEdit ( false ) ;
2020-05-20 02:54:25 -08:00
} ) ;
}
function logout _account ( ) {
2023-02-28 22:44:05 -09:00
cockpit . spawn ( [ "loginctl" , "terminate-user" , user ] ,
2020-05-20 02:54:25 -08:00
{ superuser : "try" , err : "message" } )
. then ( ( ) => {
2022-11-08 00:55:11 -09:00
get _expire ( user ) . then ( setExpiration ) ;
2020-05-20 02:54:25 -08:00
} )
. catch ( show _unexpected _error ) ;
}
2025-08-13 06:55:53 -08:00
if ( isLoading ) {
2022-12-09 03:16:42 -09:00
return (
2025-03-21 04:00:37 -08:00
< EmptyState headingLevel = "h1" titleText = { _ ( "Loading..." ) } variant = { EmptyStateVariant . sm } >
2023-04-24 06:27:34 -08:00
< EmptyStateFooter > < Spinner size = "xl" / > < / E m p t y S t a t e F o o t e r >
2022-12-09 03:16:42 -09:00
< / E m p t y S t a t e >
) ;
}
2022-11-08 00:55:11 -09:00
if ( ! expiration )
2020-05-20 02:54:25 -08:00
return null ;
const self _mod _allowed = ( user == current _user || ! ! superuser . allowed ) ;
2021-09-21 02:42:01 -08:00
let title _name = account . gecos ;
2020-05-20 02:54:25 -08:00
if ( title _name )
title _name = title _name . split ( ',' ) [ 0 ] ;
else
title _name = account . name ;
2021-09-21 02:42:01 -08:00
let last _login ;
2022-11-08 00:55:11 -09:00
if ( account . loggedIn )
2020-09-09 04:31:46 -08:00
last _login = _ ( "Logged in" ) ;
2022-11-08 00:55:11 -09:00
else if ( ! account . lastLogin )
2020-05-20 02:54:25 -08:00
last _login = _ ( "Never" ) ;
else
2022-11-08 00:55:11 -09:00
last _login = timeformat . dateTime ( new Date ( account . lastLogin ) ) ;
2020-05-20 02:54:25 -08:00
2023-04-24 06:27:34 -08:00
const actions = superuser . allowed && (
< >
< Button variant = "secondary" onClick = { ( ) => logout _account ( ) } id = "account-logout"
2025-02-03 10:49:46 -09:00
isDisabled = { ! account . loggedIn || account . uid == 0 || user === current _user } >
2023-04-24 06:27:34 -08:00
{ _ ( "Terminate session" ) }
< / B u t t o n >
{ "\n" }
2025-02-03 10:49:46 -09:00
< Button isDisabled = { account . uid == 0 || user === current _user } variant = "danger" id = "account-delete"
2023-04-24 06:27:34 -08:00
onClick = { ( ) => delete _account _dialog ( account ) } >
{ _ ( "Delete" ) }
< / B u t t o n >
< / >
) ;
2020-05-20 02:54:25 -08:00
return (
2025-07-02 02:51:01 -08:00
< Page id = "account" className = "pf-m-no-sidebar" >
2025-03-21 04:00:37 -08:00
< PageBreadcrumb hasBodyWrapper = { false } stickyOnBreakpoint = { { default : "top" } } >
2023-04-21 04:39:09 -08:00
< Breadcrumb >
< BreadcrumbItem to = "#/" > { _ ( "Accounts" ) } < / B r e a d c r u m b I t e m >
< BreadcrumbItem isActive > { title _name } < / B r e a d c r u m b I t e m >
< / B r e a d c r u m b >
< / P a g e B r e a d c r u m b >
2025-03-21 04:00:37 -08:00
< PageSection hasBodyWrapper = { false } >
2020-08-27 01:43:28 -08:00
< Gallery hasGutter >
2025-03-21 04:00:37 -08:00
< Card isPlain className = "account-details" id = "account-details" >
2023-04-24 06:27:34 -08:00
< CardHeader actions = { { actions } } >
2023-05-03 01:31:20 -08:00
< CardTitle id = "account-title" component = "h2" > { title _name } < / C a r d T i t l e >
2020-08-27 01:43:28 -08:00
< / C a r d H e a d e r >
< CardBody >
2021-05-05 00:14:18 -08:00
< Form isHorizontal onSubmit = { apply _modal _dialog } >
2020-09-09 04:31:46 -08:00
< FormGroup fieldId = "account-real-name" hasNoPaddingTop = { ! superuser . allowed } label = { _ ( "Full name" ) } >
2020-08-31 01:18:14 -08:00
{ superuser . allowed
2026-04-29 10:54:42 -08:00
? < >
< TextInput id = "account-real-name"
2026-10-14 04:02:30 -08:00
isDisabled = { committingRealName || account . uid == 0 }
2025-07-04 02:49:05 -08:00
value = { editedRealName !== null ? editedRealName : account . gecos }
2023-07-16 21:18:27 -08:00
onKeyDown = { event => {
2020-08-31 01:18:14 -08:00
if ( event . key == "Enter" ) {
event . target . blur ( ) ;
}
} }
2025-07-04 02:49:05 -08:00
onChange = { ( _event , value ) => setEditedRealName ( value ) }
2026-04-29 10:54:42 -08:00
validated = { realNameError !== "" ? "error" : "default" }
2025-07-04 02:49:05 -08:00
onBlur = { ( ) => changeRealName ( ) } / >
2026-04-29 10:54:42 -08:00
< FormHelper fieldId = "account-real-name" helperTextInvalid = { realNameError } / >
< / >
2020-08-31 01:18:14 -08:00
: < output id = "account-real-name" > { account . gecos } < / o u t p u t > }
< / F o r m G r o u p >
2020-09-09 04:31:46 -08:00
< FormGroup fieldId = "account-user-name" hasNoPaddingTop label = { _ ( "User name" ) } >
2020-08-31 01:18:14 -08:00
< output id = "account-user-name" > { account . name } < / o u t p u t >
< / F o r m G r o u p >
2022-12-12 06:49:04 -09:00
< AccountGroupsSelect key = { account . name } loggedIn = { account . loggedIn } name = { account . name } groups = { groups } / >
2020-09-09 04:31:46 -08:00
< FormGroup fieldId = "account-last-login" hasNoPaddingTop label = { _ ( "Last login" ) } >
2020-08-31 01:18:14 -08:00
< output id = "account-last-login" > { last _login } < / o u t p u t >
< / F o r m G r o u p >
2022-02-04 06:01:06 -09:00
< FormGroup fieldId = "account-locked" label = { _ ( "Options" ) } hasNoPaddingTop >
2023-03-16 09:41:20 -08:00
< Flex spaceItems = { { default : 'spaceItemsSm' } } alignItems = { { default : 'alignItemsCenter' } } >
2023-05-10 00:32:32 -08:00
< FlexItem spacer = { { default : 'spacerNone' } } >
2023-03-16 09:41:20 -08:00
< Checkbox id = "account-locked"
2025-08-13 06:55:53 -08:00
ouiaSafe = { disableLockedEdit }
isDisabled = { ! superuser . allowed || disableLockedEdit || user == current _user || isLocked == null }
isChecked = { isLocked }
2023-04-24 06:27:34 -08:00
onChange = { ( _event , checked ) => change _locked ( checked ) }
2023-03-16 09:41:20 -08:00
label = { _ ( "Disallow interactive password" ) } / >
2023-05-10 00:32:32 -08:00
< / F l e x I t e m >
2022-02-04 06:01:06 -09:00
2023-05-10 00:32:32 -08:00
< Popover bodyContent = { _ ( "Other authentication methods are still available even when interactive password authentication is not allowed." ) }
showClose = { false } >
< HelpIcon / >
< / P o p o v e r >
2023-03-16 09:41:20 -08:00
< span id = "account-expiration-text" >
{ expiration . account _text }
< / s p a n >
< Button onClick = { ( ) => account _expiration _dialog ( account , expiration . account _date ) }
isDisabled = { ! superuser . allowed }
variant = "link"
isInline
id = "account-expiration-button" >
{ _ ( "edit" ) }
< / B u t t o n >
< / F l e x >
2020-08-31 01:18:14 -08:00
< / F o r m G r o u p >
{ self _mod _allowed &&
< FormGroup fieldId = "account-set-password" label = { _ ( "Password" ) } >
2023-03-16 09:41:20 -08:00
< div className = "account-column-one" >
{ self _mod _allowed &&
< Button variant = "secondary" id = "account-set-password"
onClick = { ( ) => set _password _dialog ( account , current _user ) } >
{ _ ( "Set password" ) }
< / B u t t o n >
}
{ "\n" }
{ superuser . allowed &&
< Button variant = "secondary" id = "password-reset-button"
onClick = { ( ) => reset _password _dialog ( account ) } >
{ _ ( "Force change" ) }
< / B u t t o n >
}
2020-08-31 01:18:14 -08:00
< / d i v >
2023-03-16 09:41:20 -08:00
< Flex flex = { { default : 'inlineFlex' } } >
< span id = "password-expiration-text" >
{ expiration . password _text }
< / s p a n >
< Button onClick = { ( ) => password _expiration _dialog ( account , expiration . password _days ) }
isDisabled = { ! superuser . allowed }
variant = "link"
isInline
id = "password-expiration-button" >
{ _ ( "edit" ) }
< / B u t t o n >
< / F l e x >
2020-08-31 01:18:14 -08:00
< / F o r m G r o u p >
}
2023-01-24 05:59:49 -09:00
{ account . home && < FormGroup fieldId = "account-home-dir" hasNoPaddingTop label = { _ ( "Home directory" ) } >
< output id = "account-home-dir" > { account . home } < / o u t p u t >
< / F o r m G r o u p > }
{ account . shell && < FormGroup fieldId = "account-shell" hasNoPaddingTop label = { _ ( "Shell" ) } >
2023-07-10 07:16:48 -08:00
< Flex flex = { { default : 'inlineFlex' } } >
< output id = "account-shell" > { account . shell } < / o u t p u t >
< Button onClick = { ( ) => account _shell _dialog ( account , shells ) }
isDisabled = { ! superuser . allowed }
variant = "link"
isInline
id = "change-shell-button" >
{ _ ( "change" ) }
< / B u t t o n >
< / F l e x >
2023-01-24 05:59:49 -09:00
< / F o r m G r o u p > }
2020-08-31 01:18:14 -08:00
< / F o r m >
2020-08-27 01:43:28 -08:00
< / C a r d B o d y >
< / C a r d >
< AuthorizedKeys name = { account . name } home = { account . home } allow _mods = { self _mod _allowed } / >
2021-09-24 07:11:06 -08:00
< AccountLogs name = { account . name } / >
2020-08-27 01:43:28 -08:00
< / G a l l e r y >
< / P a g e S e c t i o n >
2022-03-01 21:19:42 -09:00
< / P a g e >
) ;
2020-05-20 02:54:25 -08:00
}
2022-12-05 03:16:25 -09:00
2024-03-01 07:05:25 -09:00
export const AccountGroupsSelect = ( { name , loggedIn , groups } ) => {
2022-12-05 03:16:25 -09:00
const [ selected , setSelected ] = useState ( ) ;
const [ primaryGroupName , setPrimaryGroupName ] = useState ( ) ;
2023-02-09 03:33:24 -09:00
const [ loading , setLoading ] = useState ( true ) ;
2024-03-01 07:05:54 -09:00
const [ modifyingGroup , setModifyingGroup ] = useState ( false ) ;
2022-12-12 06:49:04 -09:00
const [ history , setHistory ] = useState ( [ ] ) ;
2022-12-05 03:16:25 -09:00
const previousValue = useRef ( null ) ;
useEffect ( ( ) => {
const usedGroups = groups . filter ( group => group . userlist . includes ( name ) ) ;
const primaryGroup = groups . find ( group => group . userlistPrimary . includes ( name ) ) ;
2023-03-12 09:46:14 -08:00
const _primaryGroupName = primaryGroup ? . name ;
2022-12-05 03:16:25 -09:00
const _selected = usedGroups . map ( group => group . name ) ;
if ( primaryGroup )
_selected . push ( _primaryGroupName ) ;
previousValue . current = _selected ;
setSelected ( _selected ) ;
2023-02-09 03:33:24 -09:00
setLoading ( false ) ;
2022-12-05 03:16:25 -09:00
setPrimaryGroupName ( _primaryGroupName ) ;
2022-12-12 06:49:04 -09:00
} , [ groups , setSelected , name , previousValue ] ) ;
const undoGroupChanges = ( ) => {
const undoItem = history [ history . length - 1 ] ;
if ( undoItem . type === 'added' ) {
removeGroup ( undoItem . name , true ) . then ( ( ) => setHistory ( history . slice ( 0 , - 1 ) ) ) ;
} else if ( undoItem . type === 'removed' ) {
addGroup ( undoItem . name , true ) . then ( ( ) => setHistory ( history . slice ( 0 , - 1 ) ) ) ;
}
} ;
const removeGroup = ( group , isUndo ) => {
if ( ! isUndo )
setHistory ( [ ... history , { type : 'removed' , name : group } ] ) ;
2022-12-05 03:16:25 -09:00
2024-03-01 07:05:54 -09:00
setModifyingGroup ( true ) ;
2023-02-28 22:44:05 -09:00
return cockpit . spawn ( [ "gpasswd" , "-d" , name , group ] , { superuser : "require" , err : "message" } )
2022-12-05 03:16:25 -09:00
. then ( ( ) => {
2024-03-01 07:05:54 -09:00
setModifyingGroup ( false ) ;
2023-02-09 03:33:24 -09:00
} , show _unexpected _error ) ;
2022-12-05 03:16:25 -09:00
} ;
2022-12-12 06:49:04 -09:00
const addGroup = ( group , isUndo ) => {
if ( ! isUndo )
setHistory ( [ ... history , { type : 'added' , name : group } ] ) ;
2024-03-01 07:05:54 -09:00
setModifyingGroup ( true ) ;
2023-02-28 22:44:05 -09:00
return cockpit . spawn ( [ "gpasswd" , "-a" , name , group ] , { superuser : "require" , err : "message" } )
2022-12-05 03:16:25 -09:00
. then ( ( ) => {
2024-03-01 07:05:54 -09:00
setModifyingGroup ( false ) ;
2023-02-09 03:33:24 -09:00
} , show _unexpected _error ) ;
2022-12-05 03:16:25 -09:00
} ;
const chipGroupComponent = ( ) => {
return (
< LabelGroup numLabels = { 10 } >
{ ( selected || [ ] ) . map ( ( currentLabel , index ) => {
return (
< Label key = { currentLabel }
2025-03-21 04:00:37 -08:00
color = { groups . find ( group => group . name === currentLabel ) . isAdmin ? "yellow" : "blue" }
2022-12-05 03:16:25 -09:00
>
{ currentLabel }
< / L a b e l >
) ;
} ) }
< / L a b e l G r o u p >
) ;
} ;
return (
< FormGroup
fieldId = "account-groups"
id = "account-groups-form-group"
label = { _ ( "Groups" ) }
2022-12-12 06:49:04 -09:00
validated = { history . length > 0 ? "warning" : "default" }
2022-12-05 03:16:25 -09:00
>
{ superuser . allowed
2024-11-28 03:00:06 -09:00
? < MultiTypeaheadSelect
isScrollable
isDisabled = { loading || modifyingGroup }
onAdd = { val => addGroup ( val ) }
onRemove = { val => removeGroup ( val ) }
options = { groups . map ( ( option , index ) => {
return {
value : option . name ,
content : option . name ,
2025-03-21 04:00:37 -08:00
color : option . isAdmin ? "yellow" : "blue" ,
2024-11-28 03:00:06 -09:00
isDisabled : option . name == primaryGroupName ,
} ;
} ) }
selected = { selected || [ ] }
toggleProps = { { id : "account-groups" } } / >
2022-12-05 03:16:25 -09:00
: chipGroupComponent ( ) }
2023-04-17 01:51:34 -08:00
{ ( history . length > 0 )
2025-03-21 04:00:37 -08:00
? < HelperText className = "pf-v6-c-form__helper-text" >
2023-04-17 01:51:34 -08:00
< Flex >
{ loggedIn && < HelperTextItem id = "account-groups-helper" variant = "warning" > { _ ( "The user must log out and log back in for the new configuration to take effect." ) } < / H e l p e r T e x t I t e m > }
{ history . length > 0 && < Button variant = "link" id = "group-undo-btn" isInline icon = { < UndoIcon / > } onClick = { undoGroupChanges } > { _ ( "Undo" ) } < / B u t t o n > }
< / F l e x >
< / H e l p e r T e x t >
: ''
}
2022-12-05 03:16:25 -09:00
< / F o r m G r o u p >
) ;
} ;