mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-11 15:47:09 -07:00
New: Displaying folder-based permissions in UI rather than file-based permissions and with selectable sane presets
Fixed: Preserve setgid when applying unix permissions
This commit is contained in:
parent
40df88e37c
commit
e2a0b63256
21 changed files with 462 additions and 122 deletions
|
@ -5,6 +5,10 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.editableContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from '~Components/Form/Input.css';
|
||||
}
|
||||
|
@ -22,6 +26,16 @@
|
|||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.dropdownArrowContainerEditable {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding-right: 17px;
|
||||
width: 30%;
|
||||
height: 35px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdownArrowContainerDisabled {
|
||||
composes: dropdownArrowContainer;
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './EnhancedSelectInput.css';
|
||||
|
||||
function isArrowKey(keyCode) {
|
||||
|
@ -169,11 +170,21 @@ class EnhancedSelectInput extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
if (this.state.isOpen) {
|
||||
this._removeListener();
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||
const origIndex = getSelectedIndex(this.props);
|
||||
if (origIndex !== this.state.selectedIndex) {
|
||||
this.setState({ selectedIndex: origIndex });
|
||||
if (!this.props.isEditable) {
|
||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||
const origIndex = getSelectedIndex(this.props);
|
||||
|
||||
if (origIndex !== this.state.selectedIndex) {
|
||||
this.setState({ selectedIndex: origIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -297,16 +308,19 @@ class EnhancedSelectInput extends Component {
|
|||
const {
|
||||
className,
|
||||
disabledClassName,
|
||||
name,
|
||||
value,
|
||||
values,
|
||||
isDisabled,
|
||||
isEditable,
|
||||
isFetching,
|
||||
hasError,
|
||||
hasWarning,
|
||||
valueOptions,
|
||||
selectedValueOptions,
|
||||
selectedValueComponent: SelectedValueComponent,
|
||||
optionComponent: OptionComponent
|
||||
optionComponent: OptionComponent,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -332,52 +346,94 @@ class EnhancedSelectInput extends Component {
|
|||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
value={value}
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
{
|
||||
isEditable ?
|
||||
<div
|
||||
className={styles.editableContainer}
|
||||
>
|
||||
<TextInput
|
||||
className={className}
|
||||
name={name}
|
||||
value={value}
|
||||
readOnly={isDisabled}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.dropdownArrowContainerEditable,
|
||||
isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer)
|
||||
}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
<div
|
||||
className={isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</Link>
|
||||
</div> :
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
value={value}
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
<div
|
||||
className={isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
</Measure>
|
||||
</div>
|
||||
)}
|
||||
|
@ -502,6 +558,7 @@ EnhancedSelectInput.propTypes = {
|
|||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isEditable: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
valueOptions: PropTypes.object.isRequired,
|
||||
|
@ -517,6 +574,7 @@ EnhancedSelectInput.defaultProps = {
|
|||
disabledClassName: styles.isDisabled,
|
||||
isDisabled: false,
|
||||
isFetching: false,
|
||||
isEditable: false,
|
||||
valueOptions: {},
|
||||
selectedValueOptions: {},
|
||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||
|
|
|
@ -25,6 +25,7 @@ import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
|||
import TagInputConnector from './TagInputConnector';
|
||||
import TextInput from './TextInput';
|
||||
import TextTagInputConnector from './TextTagInputConnector';
|
||||
import UMaskInput from './UMaskInput';
|
||||
import styles from './FormInputGroup.css';
|
||||
|
||||
function getComponent(type) {
|
||||
|
@ -92,6 +93,9 @@ function getComponent(type) {
|
|||
case inputTypes.TEXT_TAG:
|
||||
return TextTagInputConnector;
|
||||
|
||||
case inputTypes.UMASK:
|
||||
return UMaskInput;
|
||||
|
||||
default:
|
||||
return TextInput;
|
||||
}
|
||||
|
@ -199,7 +203,7 @@ function FormInputGroup(props) {
|
|||
}
|
||||
|
||||
{
|
||||
!checkInput && helpTextWarning &&
|
||||
(!checkInput || helpText) && helpTextWarning &&
|
||||
<FormInputHelpText
|
||||
text={helpTextWarning}
|
||||
isWarning={true}
|
||||
|
|
53
frontend/src/Components/Form/UMaskInput.css
Normal file
53
frontend/src/Components/Form/UMaskInput.css
Normal file
|
@ -0,0 +1,53 @@
|
|||
.inputWrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inputFolder {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.inputUnitWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputUnit {
|
||||
composes: inputUnit from '~Components/Form/FormInputGroup.css';
|
||||
|
||||
right: 40px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 5px;
|
||||
margin-left: 17px;
|
||||
line-height: 20px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.unit {
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.readOnly {
|
||||
background-color: #eee;
|
||||
}
|
133
frontend/src/Components/Form/UMaskInput.js
Normal file
133
frontend/src/Components/Form/UMaskInput.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import styles from './UMaskInput.css';
|
||||
|
||||
const umaskOptions = [
|
||||
{
|
||||
key: '755',
|
||||
value: '755 - Owner write, Everyone else read',
|
||||
hint: 'drwxr-xr-x'
|
||||
},
|
||||
{
|
||||
key: '775',
|
||||
value: '775 - Owner & Group write, Other read',
|
||||
hint: 'drwxrwxr-x'
|
||||
},
|
||||
{
|
||||
key: '770',
|
||||
value: '770 - Owner & Group write',
|
||||
hint: 'drwxrwx---'
|
||||
},
|
||||
{
|
||||
key: '750',
|
||||
value: '750 - Owner write, Group read',
|
||||
hint: 'drwxr-x---'
|
||||
},
|
||||
{
|
||||
key: '777',
|
||||
value: '777 - Everyone write',
|
||||
hint: 'drwxrwxrwx'
|
||||
}
|
||||
];
|
||||
|
||||
function formatPermissions(permissions) {
|
||||
|
||||
const hasSticky = permissions & 0o1000;
|
||||
const hasSetGID = permissions & 0o2000;
|
||||
const hasSetUID = permissions & 0o4000;
|
||||
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const bit = (permissions & (1 << i)) !== 0;
|
||||
let digit = bit ? 'xwr'[i % 3] : '-';
|
||||
if (i === 6 && hasSetUID) {
|
||||
digit = bit ? 's' : 'S';
|
||||
} else if (i === 3 && hasSetGID) {
|
||||
digit = bit ? 's' : 'S';
|
||||
} else if (i === 0 && hasSticky) {
|
||||
digit = bit ? 't' : 'T';
|
||||
}
|
||||
result = digit + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class UMaskInput extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const valueNum = parseInt(value, 8);
|
||||
const umaskNum = 0o777 & ~valueNum;
|
||||
const umask = umaskNum.toString(8).padStart(4, '0');
|
||||
const folderNum = 0o777 & ~umaskNum;
|
||||
const folder = folderNum.toString(8).padStart(3, '0');
|
||||
const fileNum = 0o666 & ~umaskNum;
|
||||
const file = fileNum.toString(8).padStart(3, '0');
|
||||
|
||||
const unit = formatPermissions(folderNum);
|
||||
|
||||
const values = umaskOptions.map((v) => {
|
||||
return { ...v, hint: <span className={styles.unit}>{v.hint}</span> };
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.inputUnitWrapper}>
|
||||
<EnhancedSelectInput
|
||||
name={name}
|
||||
value={value}
|
||||
values={values}
|
||||
isEditable={true}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={styles.inputUnit}>
|
||||
d{unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<label>UMask</label>
|
||||
<div className={styles.value}>{umask}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Folder</label>
|
||||
<div className={styles.value}>{folder}</div>
|
||||
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>File</label>
|
||||
<div className={styles.value}>{file}</div>
|
||||
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UMaskInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
};
|
||||
|
||||
export default UMaskInput;
|
Loading…
Add table
Add a link
Reference in a new issue