@mui/material#InputAdornment TypeScript Examples
The following examples show how to use
@mui/material#InputAdornment.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: util.tsx From sapio-studio with Mozilla Public License 2.0 | 6 votes |
export function PrettyAmountField(props: { amount: number }) {
let amount = props.amount;
const max_sats = useSelector(selectMaxSats);
if (amount > max_sats) {
amount /= 100_000_000;
return (
<TextField
label="Amount (btc)"
type="text"
value={amount}
variant="outlined"
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<span
style={{ fontSize: 'larger', color: '#f2a900' }}
>
₿
</span>
</InputAdornment>
),
}}
/>
);
} else {
return (
<TextField
label="Amount (sats)"
type="text"
value={amount}
variant="outlined"
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<span
style={{ fontSize: 'larger', color: '#f2a900' }}
>
§
</span>
</InputAdornment>
),
}}
/>
);
}
}
Example #2
Source File: PasswordField.tsx From frontend with MIT License | 6 votes |
export default function PasswordField({ name = 'password', ...props }: TextFieldProps) {
const [showPassword, setShowPassword] = useState(false)
const handleShowPassword = () => setShowPassword((show) => !show)
return (
<FormTextField
name={name}
{...props}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
label="auth:fields.password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleShowPassword} edge="end">
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
/>
)
}
Example #3
Source File: TextFieldWithTooltip.tsx From Cromwell with MIT License | 6 votes |
export default function TextFieldWithTooltip(props: TextFieldProps & {
tooltipText?: string;
tooltipLink?: string;
}) {
const history = useHistory();
const openLink = () => {
if (props.tooltipLink) {
if (props.tooltipLink.startsWith('http')) {
window.open(props.tooltipLink, '_blank');
} else {
history.push(props.tooltipLink);
}
}
}
return (
<TextField
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Tooltip title={props.tooltipText}>
<IconButton onClick={openLink}>
<HelpOutlineOutlined />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
variant="standard"
{...props}
/>
)
}
Example #4
Source File: Files.tsx From NekoMaid with MIT License | 6 votes |
fileNameDialog = (title: string, dirPath: string) => dialog({
title,
content: lang.files.dialogContent,
input: {
error: true,
helperText: lang.files.invalidName,
validator: validFilename,
InputProps: { startAdornment: <InputAdornment position='start'>{dirPath}/</InputAdornment> }
}
})
Example #5
Source File: CredentialItem.tsx From console with GNU Affero General Public License v3.0 | 6 votes |
CredentialItem = ({
label = "",
value = "",
classes = {},
}: {
label: string;
value: string;
classes: any;
}) => {
return (
<div className={classes.container}>
<div className={classes.inputLabel}>{label}:</div>
<div className={classes.inputWithCopy}>
<OutlinedInput
value={value}
readOnly
endAdornment={
<InputAdornment position="end">
<CopyToClipboard text={value}>
<BoxIconButton
aria-label="copy"
tooltip={"Copy"}
onClick={() => {}}
onMouseDown={() => {}}
edge="end"
>
<CopyIcon />
</BoxIconButton>
</CopyToClipboard>
</InputAdornment>
}
/>
</div>
</div>
);
}
Example #6
Source File: PasswordElement.tsx From react-hook-form-mui with MIT License | 6 votes |
export default function PasswordElement({iconColor, ...props}: PasswordElementProps): JSX.Element {
const [password, setPassword] = useState<boolean>(true)
return (
<TextFieldElement
{...props}
InputProps={{
endAdornment: (
<InputAdornment position={'end'}>
<IconButton
onMouseDown={(e: MouseEvent<HTMLButtonElement>) =>
e.preventDefault()
}
onClick={() => setPassword(!password)}
tabIndex={-1}
color={iconColor ?? 'default'}
>
{password ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
}}
type={password ? 'password' : 'text'}
/>
)
}
Example #7
Source File: PasswordField.tsx From Cromwell with MIT License | 6 votes |
PasswordField = (props: TextFieldProps) => {
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
}
return (
<TextField {...props}
type={showPassword ? 'text' : 'password'}
variant="standard"
size="small"
className={styles.textField}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
),
}}
/>
)
}
Example #8
Source File: index.tsx From yearn-watch-legacy with GNU Affero General Public License v3.0 | 5 votes |
SearchProtocolInput = (props: SearchProtocolInputProps) => {
const [searchText, setSearchText] = useState('');
const [isSearching, setIsSearching] = useState(false);
const classes = useStyles();
const handleOnChange = (event: ChangeEvent) => {
event.preventDefault();
const value = (event.target as HTMLInputElement).value;
setSearchText(value);
};
const doSearch = () => {
props.onSearch(searchText.trim()).then(() => {
setIsSearching(false);
setSearchText('');
});
};
const handleClickSearchIcon = (event: MouseEvent<HTMLElement>) => {
event.preventDefault();
setIsSearching(true);
doSearch();
};
const handleClickSearch = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSearching(true);
doSearch();
};
const renderSearchingLabel = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let render: any;
if (isSearching) {
render = 'Searching items...';
} else {
render = ``;
}
return render;
};
const isSearchDisabled =
isSearching || searchText === undefined || searchText.trim() === '';
return (
<Container maxWidth="lg">
<form className={classes.root} onSubmit={handleClickSearch}>
<TextField
className={classes.searchInput}
id="outlined-basic"
variant="outlined"
type="text"
value={searchText}
onChange={handleOnChange}
disabled={isSearching}
placeholder="Type your terms (protocol name -ex: maker, convex-) and click on the search icon or press enter."
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="search"
disabled={isSearchDisabled}
onClick={handleClickSearchIcon}
size="large"
>
<SearchIcon />
</IconButton>
</InputAdornment>
),
}}
/>
</form>
<Container maxWidth="lg" className={classes.resultText}>
{renderSearchingLabel()}
</Container>
</Container>
);
}
Example #9
Source File: QuantityField.tsx From Cromwell with MIT License | 5 votes |
/** @internal */
export function QuantityField(props: {
value: number;
onChange: (value: number, event?: any) => any;
className?: string;
style?: React.CSSProperties;
}) {
const { value, onChange, className, style } = props;
return (
<TextField
variant="outlined"
size="small"
className={clsx(styles.QuantityField, className)}
style={style}
value={value}
onChange={(e) => {
const val = parseInt(e.target.value);
if (val && !isNaN(val) && val > 0) onChange(val, e)
}}
InputProps={{
startAdornment: (<InputAdornment position="start" style={{ margin: 0 }}>
<IconButton
aria-label="Decrease amount"
className={styles.controlButton}
style={{ width: '44px' }}
onClick={() => {
if (value > 1) {
onChange(value - 1)
}
}}
><RemoveIcon /></IconButton>
</InputAdornment>),
endAdornment: (<InputAdornment position="end" style={{ margin: 0 }}>
<IconButton
aria-label="Increase amount"
className={styles.controlButton}
style={{ width: '44px' }}
onClick={() => {
onChange(value + 1)
}}
><AddIcon /></IconButton>
</InputAdornment>),
}}
/>
)
}
Example #10
Source File: Select.tsx From Cromwell with MIT License | 5 votes |
export function Select(props: { options?: ({ value: string | number | undefined; label: string; } | string | number | undefined)[]; selectStyle?: React.CSSProperties; selectClassName?: string; tooltipText?: string; tooltipLink?: string; } & SelectProps<string | number>) { const history = useHistory(); const openLink = () => { if (props.tooltipLink) { if (props.tooltipLink.startsWith('http')) { window.open(props.tooltipLink, '_blank'); } else { history.push(props.tooltipLink); } } } return ( <FormControl fullWidth={props.fullWidth} style={props.style} className={props.className} > <InputLabel style={props.variant === 'standard' ? { marginLeft: '-15px', marginTop: '8px', } : undefined} >{props.label}</InputLabel> <MuiSelect {...props} className={props.selectClassName} style={props.selectStyle} MenuProps={{ style: { zIndex: 10001 } }} endAdornment={( (props.tooltipText || props.tooltipLink) && ( <InputAdornment position="end" sx={{ mr: 1 }}> <Tooltip title={props.tooltipText}> <IconButton onClick={openLink}> <HelpOutlineOutlined /> </IconButton> </Tooltip> </InputAdornment> ) )} > {props.options?.map((option) => { const label = typeof option === 'object' ? option.label : option; const value = typeof option === 'object' ? option.value : option; return ( <MenuItem value={value} key={value + ''}>{label}</MenuItem> ) })} </MuiSelect> </FormControl> ) }
Example #11
Source File: LoginPage.tsx From console with GNU Affero General Public License v3.0 | 4 votes |
Login = ({ classes }: ILoginProps) => {
const dispatch = useDispatch();
const [accessKey, setAccessKey] = useState<string>("");
const [jwt, setJwt] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [loginStrategy, setLoginStrategy] = useState<ILoginDetails>({
loginStrategy: loginStrategyType.unknown,
redirect: "",
});
const [loginSending, setLoginSending] = useState<boolean>(false);
const [loadingFetchConfiguration, setLoadingFetchConfiguration] =
useState<boolean>(true);
const [latestMinIOVersion, setLatestMinIOVersion] = useState<string>("");
const [loadingVersion, setLoadingVersion] = useState<boolean>(true);
const loginStrategyEndpoints: LoginStrategyRoutes = {
form: "/api/v1/login",
"service-account": "/api/v1/login/operator",
};
const loginStrategyPayload: LoginStrategyPayload = {
form: { accessKey, secretKey },
"service-account": { jwt },
};
const fetchConfiguration = () => {
setLoadingFetchConfiguration(true);
};
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoginSending(true);
api
.invoke(
"POST",
loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login",
loginStrategyPayload[loginStrategy.loginStrategy]
)
.then(() => {
// We set the state in redux
dispatch(userLogged(true));
if (loginStrategy.loginStrategy === loginStrategyType.form) {
localStorage.setItem("userLoggedIn", accessKey);
}
let targetPath = "/";
if (
localStorage.getItem("redirect-path") &&
localStorage.getItem("redirect-path") !== ""
) {
targetPath = `${localStorage.getItem("redirect-path")}`;
localStorage.setItem("redirect-path", "");
}
history.push(targetPath);
})
.catch((err) => {
setLoginSending(false);
dispatch(setErrorSnackMessage(err));
});
};
useEffect(() => {
if (loadingFetchConfiguration) {
api
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
setLoginStrategy(loginDetails);
setLoadingFetchConfiguration(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingFetchConfiguration(false);
});
}
}, [loadingFetchConfiguration, dispatch]);
useEffect(() => {
if (loadingVersion) {
api
.invoke("GET", "/api/v1/check-version")
.then(
({
current_version,
latest_version,
}: {
current_version: string;
latest_version: string;
}) => {
setLatestMinIOVersion(latest_version);
setLoadingVersion(false);
}
)
.catch((err: ErrorResponseHandler) => {
// try the operator version
api
.invoke("GET", "/api/v1/check-operator-version")
.then(
({
current_version,
latest_version,
}: {
current_version: string;
latest_version: string;
}) => {
setLatestMinIOVersion(latest_version);
setLoadingVersion(false);
}
)
.catch((err: ErrorResponseHandler) => {
setLoadingVersion(false);
});
});
}
}, [loadingVersion, setLoadingVersion, setLatestMinIOVersion]);
let loginComponent = null;
switch (loginStrategy.loginStrategy) {
case loginStrategyType.form: {
loginComponent = (
<React.Fragment>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
<Grid item xs={12} className={classes.spacerBottom}>
<LoginField
fullWidth
id="accessKey"
className={classes.inputField}
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAccessKey(e.target.value)
}
placeholder={"Username"}
name="accessKey"
autoComplete="username"
disabled={loginSending}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
className={classes.iconColor}
>
<UserFilledIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<LoginField
fullWidth
className={classes.inputField}
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSecretKey(e.target.value)
}
name="secretKey"
type="password"
id="secretKey"
autoComplete="current-password"
disabled={loginSending}
placeholder={"Password"}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
className={classes.iconColor}
>
<LockFilledIcon />
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.submitContainer}>
<Button
type="submit"
variant="contained"
color="primary"
id="do-login"
className={classes.submit}
disabled={secretKey === "" || accessKey === "" || loginSending}
>
Login
</Button>
</Grid>
<Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />}
</Grid>
</form>
</React.Fragment>
);
break;
}
case loginStrategyType.redirect:
case loginStrategyType.redirectServiceAccount: {
loginComponent = (
<React.Fragment>
<Button
component={"a"}
href={loginStrategy.redirect}
type="submit"
variant="contained"
color="primary"
id="sso-login"
className={classes.submit}
>
Login with SSO
</Button>
</React.Fragment>
);
break;
}
case loginStrategyType.serviceAccount: {
loginComponent = (
<React.Fragment>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
<Grid item xs={12}>
<LoginField
required
className={classes.inputField}
fullWidth
id="jwt"
value={jwt}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setJwt(e.target.value)
}
name="jwt"
autoComplete="off"
disabled={loginSending}
placeholder={"Enter JWT"}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LockIcon />
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.submitContainer}>
<Button
type="submit"
variant="contained"
color="primary"
id="do-login"
className={classes.submit}
disabled={jwt === "" || loginSending}
>
Login
</Button>
</Grid>
<Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />}
</Grid>
</form>
</React.Fragment>
);
break;
}
default:
loginComponent = (
<div style={{ textAlign: "center" }}>
{loadingFetchConfiguration ? (
<Loader className={classes.loadingLoginStrategy} />
) : (
<React.Fragment>
<div>
<p style={{ color: "#000", textAlign: "center" }}>
An error has occurred
<br />
The backend cannot be reached.
</p>
</div>
<div>
<Button
onClick={() => {
fetchConfiguration();
}}
endIcon={<RefreshIcon />}
color={"primary"}
variant="outlined"
id="retry"
className={classes.retryButton}
>
Retry
</Button>
</div>
</React.Fragment>
)}
</div>
);
}
const isOperator =
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
const consoleText = isOperator ? <OperatorLogo /> : <ConsoleLogo />;
const hyperLink = isOperator
? "https://docs.min.io/minio/k8s/operator-console/operator-console.html?ref=con"
: "https://docs.min.io/minio/baremetal/console/minio-console.html?ref=con";
const theme = useTheme();
return (
<div className={classes.root}>
<CssBaseline />
<MainError />
<div className={classes.loginPage}>
<Grid
container
style={{
maxWidth: 400,
margin: "auto",
}}
>
<Grid
item
xs={12}
style={{
background:
"transparent linear-gradient(180deg, #FBFAFA 0%, #E4E4E4 100%) 0% 0% no-repeat padding-box",
padding: 40,
color: theme.palette.primary.main,
}}
sx={{
marginTop: {
md: 16,
sm: 8,
xs: 3,
},
}}
>
<Box className={classes.iconLogo}>{consoleText}</Box>
<Box
style={{
font: "normal normal normal 20px/24px Lato",
}}
>
Multicloud Object Storage
</Box>
</Grid>
<Grid
item
xs={12}
style={{
backgroundColor: "white",
padding: 40,
color: theme.palette.primary.main,
}}
>
{loginComponent}
<Box
style={{
textAlign: "center",
marginTop: 20,
}}
>
<a
href={hyperLink}
target="_blank"
rel="noreferrer"
style={{
color: theme.colors.link,
font: "normal normal normal 12px/15px Lato",
}}
>
Learn more about {isOperator ? "OPERATOR CONSOLE" : "CONSOLE"}
</a>
<a
href={hyperLink}
target="_blank"
rel="noreferrer"
style={{
color: theme.colors.link,
font: "normal normal normal 12px/15px Lato",
textDecoration: "none",
fontWeight: "bold",
paddingLeft: 4,
}}
>
➔
</a>
</Box>
</Grid>
<Grid item xs={12} className={classes.linkHolder}>
<div className={classes.miniLinks}>
<a
href="https://docs.min.io/?ref=con"
target="_blank"
rel="noreferrer"
>
<DocumentationIcon /> Documentation
</a>
<span className={classes.separator}>|</span>
<a
href="https://github.com/minio/minio"
target="_blank"
rel="noreferrer"
>
<GithubIcon /> Github
</a>
<span className={classes.separator}>|</span>
<a
href="https://subnet.min.io/?ref=con"
target="_blank"
rel="noreferrer"
>
<SupportMenuIcon /> Support
</a>
<span className={classes.separator}>|</span>
<a
href="https://min.io/download/?ref=con"
target="_blank"
rel="noreferrer"
>
<DownloadIcon /> Download
</a>
</div>
<div className={clsx(classes.miniLinks, classes.miniLogo)}>
<a
href={"https://github.com/minio/minio/releases"}
target="_blank"
rel="noreferrer"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 20,
}}
>
<MinIOTierIconXs /> <b>Latest Version:</b>
{!loadingVersion && latestMinIOVersion !== "" && (
<React.Fragment>{latestMinIOVersion}</React.Fragment>
)}
</a>
</div>
</Grid>
</Grid>
</div>
</div>
);
}
Example #12
Source File: CustomIdField.tsx From firecms with MIT License | 4 votes |
export function CustomIdField<M, UserType>
({ schema, status, onChange, error, entity }: {
schema: EntitySchema<M>,
status: EntityStatus,
onChange: Function,
error: boolean,
entity: Entity<M> | undefined
}) {
const classes = formStyles();
const disabled = status === "existing" || !schema.customId;
const idSetAutomatically = status !== "existing" && !schema.customId;
const hasEnumValues = typeof schema.customId === "object";
const snackbarContext = useSnackbarController();
const { copy } = useClipboard({
onSuccess: (text) => snackbarContext.open({
type: "success",
message: `Copied ${text}`
})
});
const appConfig: FireCMSContext<UserType> | undefined = useFireCMSContext();
const inputProps = {
className: classes.input,
endAdornment: entity
? (
<InputAdornment position="end">
<IconButton onClick={(e) => copy(entity.id)}
aria-label="copy-id"
size="large">
<Tooltip title={"Copy"}>
<svg
className={"MuiSvgIcon-root MuiSvgIcon-fontSizeSmall"}
fill={"currentColor"}
width="20" height="20" viewBox="0 0 24 24">
<path
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</Tooltip>
</IconButton>
{appConfig?.entityLinkBuilder &&
<a href={appConfig.entityLinkBuilder({ entity })}
rel="noopener noreferrer"
target="_blank">
<IconButton onClick={(e) => e.stopPropagation()}
aria-label="go-to-datasource" size="large">
<Tooltip title={"Open in the console"}>
<OpenInNewIcon fontSize={"small"}/>
</Tooltip>
</IconButton>
</a>}
</InputAdornment>
)
: undefined
};
const fieldProps: any = {
label: idSetAutomatically ? "ID is set automatically" : "ID",
disabled: disabled,
name: "id",
type: null,
value: entity && status === "existing" ? entity.id : undefined,
variant: "filled"
};
return (
<FormControl fullWidth
error={error}
{...fieldProps}
key={"custom-id-field"}>
{hasEnumValues && schema.customId &&
<>
<InputLabel id={"id-label"}>{fieldProps.label}</InputLabel>
<MuiSelect
labelId={"id-label"}
className={classes.select}
error={error}
{...fieldProps}
onChange={(event: any) => onChange(event.target.value)}>
{Object.entries(schema.customId).map(([key, label]) =>
<MenuItem
key={`custom-id-item-${key}`}
value={key}>
{`${key} - ${label}`}
</MenuItem>)}
</MuiSelect>
</>}
{!hasEnumValues &&
<MuiTextField {...fieldProps}
error={error}
InputProps={inputProps}
helperText={schema.customId === "optional" ? "Leave this blank to autogenerate an ID" : "ID of the new document"}
onChange={(event) => {
let value = event.target.value;
if (value) value = value.trim();
return onChange(value.length ? value : undefined);
}}/>}
<ErrorMessage name={"id"}
component="div">
{(_) => "You need to specify an ID"}
</ErrorMessage>
</FormControl>
);
}
Example #13
Source File: index.tsx From yearn-watch-legacy with GNU Affero General Public License v3.0 | 4 votes |
SearchInput = (props: SearchInputProps) => {
const {
onFilter,
debounceWait,
totalItems,
foundItems,
totalSubItems,
foundSubItems,
} = props;
const [searchText, setSearchText] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [filterVaultsWithWarnings, setFilterVaultsWithWarnings] =
useState(false);
const [healthCheckFilter, setHealthCheckFilter] = useState('');
const debounceFilter = useCallback(
debounce((newSearchText, flags) => {
const newSearchTextLowerCase = newSearchText.toLowerCase();
onFilter(newSearchTextLowerCase, flags, healthCheckFilter);
setIsSearching(false);
}, debounceWait),
[debounceWait, isSearching]
);
// Event listener called on every change
const onChange = useCallback(
(event: ChangeEvent) => {
const value = (event.target as HTMLInputElement).value;
setIsSearching(true);
setSearchText(value);
debounceFilter(value, getCurrentFlags(filterVaultsWithWarnings));
},
[filterVaultsWithWarnings, searchText, isSearching, healthCheckFilter]
);
const onFilterVaultsWithWarnings = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setFilterVaultsWithWarnings(e.target.checked);
setIsSearching(true);
const newSearchTextLowerCase = searchText.toLowerCase();
onFilter(
newSearchTextLowerCase,
getCurrentFlags(e.target.checked),
healthCheckFilter
);
setIsSearching(false);
},
[searchText, isSearching, healthCheckFilter]
);
const handleClickClearSearch = useCallback(() => {
setSearchText('');
setFilterVaultsWithWarnings(false);
onFilter('', getCurrentFlags(false), '');
}, [onFilter]);
const healthCheckFilterChange = useCallback(
(e: SelectChangeEvent<unknown>) => {
setHealthCheckFilter((e.target as HTMLInputElement).value);
setIsSearching(true);
const newSearchTextLowerCase = searchText.toLowerCase();
onFilter(
newSearchTextLowerCase,
getCurrentFlags(filterVaultsWithWarnings),
(e.target as HTMLInputElement).value
);
setIsSearching(false);
},
[searchText, healthCheckFilter, isSearching]
);
const renderSearchingLabel = useCallback(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let render: any;
if (isSearching) {
render = (
<div>
<ProgressSpinnerBar label="results" />
</div>
);
} else {
render = (
<>
{healthCheckFilter !== '' && (
<WarningLabel
warningText={'HealthCheck Filter is ON!'}
/>
)}
<ResultsLabel
title="Vaults"
totalItems={totalItems}
foundItems={foundItems}
displayFound={true}
isSearching={isSearching}
/>
<ResultsLabel
title="Strategies"
totalItems={totalSubItems}
foundItems={foundSubItems}
displayFound={true}
isSearching={isSearching}
/>
</>
);
}
return render;
}, [isSearching, totalItems, foundItems, totalSubItems, foundSubItems]);
return (
<div>
<StyledForm>
<Grid container direction="row" alignItems="center" spacing={3}>
<Grid item xs={12} sm={6}>
<StyledContainer maxWidth="lg">
<StyledTextField
variant="outlined"
onChange={onChange}
type="search"
value={searchText}
placeholder="Search by vault/strategy address/name, strategist address, token name/symbol, share token symbol/name or API version."
InputProps={
searchText == ''
? {
startAdornment: (
<InputAdornment position="end">
<Search />
</InputAdornment>
),
}
: {
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="delete"
onClick={
handleClickClearSearch
}
size="large"
>
<Delete />
</IconButton>
</InputAdornment>
),
}
}
/>
</StyledContainer>
</Grid>
<Grid item xs={12} sm={3}>
<StyledContainer maxWidth="lg">
<StyledFormControlLabel
control={
<Switch
checked={filterVaultsWithWarnings}
onChange={onFilterVaultsWithWarnings}
color="primary"
/>
}
labelPlacement="start"
label="Vaults with warnings"
/>
</StyledContainer>
</Grid>
<Grid item xs={12} sm={3}>
<StyledContainer maxWidth="lg">
<StyledFormControlLabel
control={
<StyledSelect
displayEmpty
variant="standard"
defaultValue=""
value={healthCheckFilter}
onChange={healthCheckFilterChange}
>
<MenuItem value="">All</MenuItem>
<MenuItem value="Enabled">
Enabled
</MenuItem>
<MenuItem value="Disabled">
Disabled
</MenuItem>
<MenuItem value="None">
Not Set
</MenuItem>
</StyledSelect>
}
labelPlacement="start"
label="HealthCheck"
/>
</StyledContainer>
</Grid>
</Grid>
</StyledForm>
<StyledContainerResult maxWidth="lg">
{renderSearchingLabel()}
</StyledContainerResult>
</div>
);
}
Example #14
Source File: Withdraw.tsx From wrap.scrt.network with MIT License | 4 votes |
export default function Withdraw({
token,
secretjs,
secretAddress,
balances,
onSuccess,
onFailure,
}: {
token: Token;
secretjs: SecretNetworkClient | null;
secretAddress: string;
balances: Map<string, string>;
onSuccess: (txhash: string) => any;
onFailure: (error: any) => any;
}) {
const [targetAddress, setTargetAddress] = useState<string>("");
const [loadingTx, setLoading] = useState<boolean>(false);
const [selectedChainIndex, setSelectedChainIndex] = useState<number>(0);
const inputRef = useRef<any>();
const maxButtonRef = useRef<any>();
const sourceChain = chains["Secret Network"];
const targetChain =
chains[token.withdrawals[selectedChainIndex].target_chain_name];
const availableBalance =
balances.get(token.withdrawals[selectedChainIndex].from_denom) || "";
useEffect(() => {
(async () => {
while (!window.keplr || !window.getOfflineSignerOnlyAmino) {
await sleep(100);
}
// Find address on target chain
const { chain_id: targetChainId } =
chains[token.withdrawals[selectedChainIndex].target_chain_name];
if (token.withdrawals[selectedChainIndex].target_chain_name === "Terra") {
await suggestTerraToKeplr(window.keplr);
}
await window.keplr.enable(targetChainId);
const targetOfflineSigner =
window.getOfflineSignerOnlyAmino(targetChainId);
const targetFromAccounts = await targetOfflineSigner.getAccounts();
setTargetAddress(targetFromAccounts[0].address);
})();
}, [selectedChainIndex]);
return (
<>
<div style={{ padding: "1.5em" }}>
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.5em",
}}
>
<Typography>
Withdraw <strong>{token.name}</strong> from{" "}
<strong>Secret Network</strong> to
</Typography>
<If condition={token.withdrawals.length === 1}>
<Then>
<Typography sx={{ marginLeft: "-0.2em" }}>
<strong>
{token.withdrawals[selectedChainIndex].target_chain_name}
</strong>
</Typography>
</Then>
<Else>
<FormControl>
<Select
value={selectedChainIndex}
onChange={(e) =>
setSelectedChainIndex(Number(e.target.value))
}
>
{token.withdrawals.map((chain, index) => (
<MenuItem value={index} key={index}>
<div
style={{
display: "flex",
gap: "0.5em",
placeItems: "center",
}}
>
<Avatar
src={chains[chain.target_chain_name].chain_image}
sx={{
marginLeft: "0.3em",
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
<strong>{chain.target_chain_name}</strong>
</div>
</MenuItem>
))}
</Select>
</FormControl>
</Else>
</If>
</div>
<br />
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>From:</Typography>
<CopyableAddress
address={secretAddress}
explorerPrefix={sourceChain.explorer_account}
/>
</div>
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>To:</Typography>
<CopyableAddress
address={targetAddress}
explorerPrefix={targetChain.explorer_account}
/>
</div>
<br />
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.3em",
marginBottom: "0.8em",
}}
>
<Typography sx={{ fontSize: "0.8em", fontWeight: "bold" }}>
Available to Withdraw:
</Typography>
<Typography
sx={{
fontSize: "0.8em",
opacity: 0.8,
cursor: "pointer",
}}
onClick={() => {
maxButtonRef.current.click();
}}
>
{(() => {
if (availableBalance === "") {
return <CircularProgress size="0.6em" />;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return "Error";
}
return `${prettyBalance} ${token.name}`;
})()}
</Typography>
</div>
<FormControl sx={{ width: "100%" }} variant="standard">
<InputLabel htmlFor="Amount to Withdraw">
Amount to Withdraw
</InputLabel>
<Input
autoFocus
id="Amount to Withdraw"
fullWidth
type="text"
inputRef={inputRef}
startAdornment={
<InputAdornment position="start">
<Avatar
src={token.image}
sx={{
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<Button
ref={maxButtonRef}
style={{
padding: "0.1em 0.5em",
minWidth: 0,
}}
onClick={() => {
if (availableBalance === "") {
return;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return;
}
inputRef.current.value = prettyBalance;
}}
>
MAX
</Button>
</InputAdornment>
}
/>
</FormControl>
</div>
<div
style={{
display: "flex",
placeContent: "center",
marginBottom: "0.4em",
}}
>
<LoadingButton
variant="contained"
sx={{
padding: "0.5em 0",
width: "10em",
fontWeight: "bold",
fontSize: "1.2em",
}}
loading={loadingTx}
onClick={async () => {
if (!secretjs) {
console.error("No secretjs");
return;
}
if (!inputRef?.current?.value) {
console.error("Empty withdraw");
return;
}
const normalizedAmount = (inputRef.current.value as string).replace(
/,/g,
""
);
if (!(Number(normalizedAmount) > 0)) {
console.error(`${normalizedAmount} not bigger than 0`);
return;
}
setLoading(true);
const amount = new BigNumber(normalizedAmount)
.multipliedBy(`1e${token.decimals}`)
.toFixed(0, BigNumber.ROUND_DOWN);
const { withdraw_channel_id, withdraw_gas } =
chains[token.withdrawals[selectedChainIndex].target_chain_name];
try {
const tx = await secretjs.tx.broadcast(
[
new MsgTransfer({
sender: secretAddress,
receiver: targetAddress,
sourceChannel: withdraw_channel_id,
sourcePort: "transfer",
token: {
amount,
denom: token.withdrawals[selectedChainIndex].from_denom,
},
timeoutTimestampSec: String(
Math.floor(Date.now() / 1000) + 15 * 60
), // 15 minute timeout
}),
],
{
gasLimit: withdraw_gas,
gasPriceInFeeDenom: 0.25,
feeDenom: "uscrt",
}
);
if (tx.code === 0) {
inputRef.current.value = "";
onSuccess(tx.transactionHash);
} else {
onFailure(tx.rawLog);
}
} catch (e) {
onFailure(e);
} finally {
setLoading(false);
}
}}
>
Withdraw
</LoadingButton>
</div>
</>
);
}
Example #15
Source File: Deposit.tsx From wrap.scrt.network with MIT License | 4 votes |
export default function Deposit({
token,
secretAddress,
onSuccess,
onFailure,
}: {
token: Token;
secretAddress: string;
onSuccess: (txhash: string) => any;
onFailure: (error: any) => any;
}) {
const [sourceAddress, setSourceAddress] = useState<string>("");
const [availableBalance, setAvailableBalance] = useState<string>("");
const [loadingTx, setLoading] = useState<boolean>(false);
const [sourceCosmJs, setSourceCosmJs] =
useState<SigningStargateClient | null>(null);
const [selectedChainIndex, setSelectedChainIndex] = useState<number>(0);
const [fetchBalanceInterval, setFetchBalanceInterval] = useState<any>(null);
const inputRef = useRef<any>();
const maxButtonRef = useRef<any>();
const sourceChain =
chains[token.deposits[selectedChainIndex].source_chain_name];
const targetChain = chains["Secret Network"];
const fetchSourceBalance = async (sourceAddress: string) => {
const url = `${
chains[token.deposits[selectedChainIndex].source_chain_name].lcd
}/bank/balances/${sourceAddress}`;
try {
const response = await fetch(url);
const result: {
height: string;
result: Array<{ denom: string; amount: string }>;
} = await response.json();
const balance =
result.result.find(
(c) => c.denom === token.deposits[selectedChainIndex].from_denom
)?.amount || "0";
setAvailableBalance(balance);
} catch (e) {
console.error(`Error while trying to query ${url}:`, e);
setAvailableBalance("Error");
}
};
useEffect(() => {
setAvailableBalance("");
if (!sourceAddress) {
return;
}
if (fetchBalanceInterval) {
clearInterval(fetchBalanceInterval);
}
fetchSourceBalance(sourceAddress);
const interval = setInterval(
() => fetchSourceBalance(sourceAddress),
10_000
);
setFetchBalanceInterval(interval);
return () => clearInterval(interval);
}, [sourceAddress]);
useEffect(() => {
(async () => {
while (!window.keplr || !window.getOfflineSignerOnlyAmino) {
await sleep(100);
}
if (["LUNA", "UST"].includes(token.name.toUpperCase())) {
await suggestTerraToKeplr(window.keplr);
}
// Initialize cosmjs on the target chain, because it has sendIbcTokens()
const { chain_id, rpc, bech32_prefix } =
chains[token.deposits[selectedChainIndex].source_chain_name];
await window.keplr.enable(chain_id);
const sourceOfflineSigner = window.getOfflineSignerOnlyAmino(chain_id);
const depositFromAccounts = await sourceOfflineSigner.getAccounts();
setSourceAddress(depositFromAccounts[0].address);
const cosmjs = await SigningStargateClient.connectWithSigner(
rpc,
sourceOfflineSigner,
{ prefix: bech32_prefix, broadcastPollIntervalMs: 10_000 }
);
setSourceCosmJs(cosmjs);
})();
}, [selectedChainIndex]);
return (
<>
<div style={{ padding: "1.5em" }}>
<div
style={{
display: "flex",
placeItems: "center",
gap: token.deposits.length === 1 ? "0.3em" : "0.5em",
}}
>
<Typography>
Deposit <strong>{token.name}</strong> from
</Typography>
<If condition={token.deposits.length === 1}>
<Then>
<Typography>
<strong>
{token.deposits[selectedChainIndex].source_chain_name}
</strong>
</Typography>
</Then>
<Else>
<FormControl>
<Select
value={selectedChainIndex}
onChange={(e) =>
setSelectedChainIndex(Number(e.target.value))
}
>
{token.deposits.map((chain, index) => (
<MenuItem value={index} key={index}>
<div
style={{
display: "flex",
gap: "0.5em",
placeItems: "center",
}}
>
<Avatar
src={chains[chain.source_chain_name].chain_image}
sx={{
marginLeft: "0.3em",
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
<strong>{chain.source_chain_name}</strong>
</div>
</MenuItem>
))}
</Select>
</FormControl>
</Else>
</If>
<Typography>
to <strong>Secret Network</strong>
</Typography>
</div>
<br />
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>From:</Typography>
<CopyableAddress
address={sourceAddress}
explorerPrefix={sourceChain.explorer_account}
/>
</div>
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>To:</Typography>
<CopyableAddress
address={secretAddress}
explorerPrefix={targetChain.explorer_account}
/>
</div>
<br />
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.3em",
marginBottom: "0.8em",
}}
>
<Typography sx={{ fontSize: "0.8em", fontWeight: "bold" }}>
Available to Deposit:
</Typography>
<Typography
sx={{
fontSize: "0.8em",
opacity: 0.8,
cursor: "pointer",
}}
onClick={() => {
maxButtonRef.current.click();
}}
>
{(() => {
if (availableBalance === "") {
return <CircularProgress size="0.6em" />;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return "Error";
}
return `${prettyBalance} ${token.name}`;
})()}
</Typography>
</div>
<FormControl sx={{ width: "100%" }} variant="standard">
<InputLabel htmlFor="Amount to Deposit">Amount to Deposit</InputLabel>
<Input
autoFocus
id="Amount to Deposit"
fullWidth
type="text"
inputRef={inputRef}
startAdornment={
<InputAdornment position="start">
<Avatar
src={token.image}
sx={{
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<Button
ref={maxButtonRef}
style={{
padding: "0.1em 0.5em",
minWidth: 0,
}}
onClick={() => {
if (availableBalance === "") {
return;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return;
}
inputRef.current.value = prettyBalance;
}}
>
MAX
</Button>
</InputAdornment>
}
/>
</FormControl>
</div>
<div
style={{
display: "flex",
placeContent: "center",
marginBottom: "0.4em",
}}
>
<LoadingButton
variant="contained"
sx={{
padding: "0.5em 0",
width: "10em",
fontWeight: "bold",
fontSize: "1.2em",
}}
loading={loadingTx}
onClick={async () => {
if (!sourceCosmJs) {
console.error("No cosmjs");
return;
}
if (!inputRef?.current?.value) {
console.error("Empty deposit");
return;
}
const normalizedAmount = (inputRef.current.value as string).replace(
/,/g,
""
);
if (!(Number(normalizedAmount) > 0)) {
console.error(`${normalizedAmount} not bigger than 0`);
return;
}
setLoading(true);
const amount = new BigNumber(normalizedAmount)
.multipliedBy(`1e${token.decimals}`)
.toFixed(0, BigNumber.ROUND_DOWN);
const { deposit_channel_id, deposit_gas } =
chains[token.deposits[selectedChainIndex].source_chain_name];
try {
const { transactionHash } = await sourceCosmJs.sendIbcTokens(
sourceAddress,
secretAddress,
{
amount,
denom: token.deposits[selectedChainIndex].from_denom,
},
"transfer",
deposit_channel_id,
undefined,
Math.floor(Date.now() / 1000) + 15 * 60, // 15 minute timeout
gasToFee(deposit_gas)
);
inputRef.current.value = "";
onSuccess(transactionHash);
} catch (e) {
onFailure(e);
} finally {
setLoading(false);
}
}}
>
Deposit
</LoadingButton>
</div>
</>
);
}
Example #16
Source File: ColorPicker.tsx From Cromwell with MIT License | 4 votes |
export function ColorPicker(props: {
label?: string;
value?: string;
className?: string;
style?: React.CSSProperties;
onChange?: (color: string) => void;
}) {
const colorRef = useRef<string | null>(null);
const prevValue = useRef<string | null>(null);
const inputAnchorRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
const forceUpdate = useForceUpdate();
if (props.value !== prevValue.current) {
prevValue.current = props.value;
colorRef.current = props.value;
}
const handleChange = (color: { hex: string; rgb: { r: number; g: number; b: number; a: number } }) => {
const colorStr = color.rgb.a === 1 ? color.hex : `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`;
colorRef.current = colorStr;
forceUpdate();
}
const handleClose = () => {
handleApply();
setOpen(false);
}
const handleApply = () => {
props.onChange?.(colorRef.current);
}
const handleInputChange = (event) => {
colorRef.current = event.target.value;
forceUpdate();
handleApply();
}
return (
<>
<TextField
InputProps={{
startAdornment: (
<InputAdornment position="start">
<div style={{ backgroundColor: colorRef.current, width: '20px', height: '20px', borderRadius: '100%' }}></div>
</InputAdornment>
),
}}
variant="standard"
className={props.className}
label={props.label}
fullWidth
value={colorRef.current}
ref={inputAnchorRef}
onChange={handleInputChange}
onClick={() => setOpen(true)}
style={props.style}
/>
<Popover
open={open}
anchorEl={inputAnchorRef.current}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<div>
<SketchPicker
color={colorRef.current ?? '#000'}
onChangeComplete={handleChange}
/>
</div>
</Popover>
</>
)
}
Example #17
Source File: FirstStep.tsx From frontend with MIT License | 4 votes |
export default function FirstStep() {
const { data: prices } = useSinglePriceList()
const { t } = useTranslation('one-time-donation')
const mobile = useMediaQuery('(max-width:568px)')
const options = [
{ value: 'card', label: t('third-step.card') },
{ value: 'bank', label: t('third-step.bank-payment') },
]
const [paymentField] = useField('payment')
const [amount] = useField('amount')
const { campaign } = useContext(StepsContext)
const bankAccountInfo = {
owner: t('third-step.owner'),
bank: t('third-step.bank'),
iban: ibanNumber,
}
return (
<Root>
<Typography variant="h4">{t('third-step.title')}</Typography>
<Box marginTop={theme.spacing(4)}>
<RadioButtonGroup name="payment" options={options} />
</Box>
<Collapse in={paymentField.value === 'bank'} timeout="auto">
<List component="div" disablePadding>
<Typography marginTop={theme.spacing(8)} variant="h6">
{t('third-step.bank-details')}
</Typography>
<Divider className={classes.divider} />
<Grid container justifyContent="center">
<Grid my={2} item display="flex" justifyContent="space-between" xs={9}>
<Typography>{bankAccountInfo.owner}</Typography>
<CopyTextButton
label={t('third-step.btn-copy')}
text={bankAccountInfo.owner}
variant="contained"
size="small"
color="info"
/>
</Grid>
<Grid my={2} item display="flex" justifyContent="space-between" xs={9}>
<Typography>{bankAccountInfo.bank}</Typography>
<CopyTextButton
label={t('third-step.btn-copy')}
text={bankAccountInfo.bank}
variant="contained"
size="small"
color="info"
/>
</Grid>
<Grid my={2} item display="flex" justifyContent="space-between" xs={9}>
<Typography>{ibanNumber}</Typography>
<CopyTextButton
label={t('third-step.btn-copy')}
text={bankAccountInfo.iban}
variant="contained"
size="small"
color="info"
/>
</Grid>
</Grid>
<Typography my={2} variant="h6">
{t('third-step.reason-donation')}
</Typography>
<Divider className={classes.divider} />
<Grid container justifyContent="center">
<Grid my={3} item display="flex" justifyContent="space-between" xs={9}>
<Alert severity="warning">
<Typography fontWeight="bold">{campaign.bankHash}</Typography>
</Alert>
<CopyTextButton
text={campaign.title}
variant="contained"
color="info"
size="small"
label={t('third-step.btn-copy')}
/>
</Grid>
</Grid>
<Typography>{t('third-step.message-warning')}</Typography>
</List>
</Collapse>
<Collapse in={paymentField.value === 'card'} timeout="auto">
<Typography variant="h4" sx={{ marginTop: theme.spacing(8) }}>
{t('first-step.amount')}
</Typography>
<Box marginTop={theme.spacing(4)}>
<RadioButtonGroup
name="amount"
options={
prices
?.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount))
.map((v) => ({
label: money(Number(v.unit_amount)),
value: v.id,
}))
.concat({ label: 'Other', value: 'other' }) || []
}
/>
<Collapse in={amount.value === 'other'} timeout="auto">
<Grid
style={
!mobile
? {
float: 'right',
marginTop: theme.spacing(-10),
width: '49%',
}
: { marginTop: theme.spacing(2) }
}>
<FormTextField
name="otherAmount"
type="number"
label={t('first-step.amount')}
InputProps={{
style: { fontSize: 20, padding: 16 },
endAdornment: (
<InputAdornment variant="filled" position="end">
Лв.
</InputAdornment>
),
}}
/>
</Grid>
</Collapse>
</Box>
</Collapse>
</Root>
)
}
Example #18
Source File: Coupon.tsx From Cromwell with MIT License | 4 votes |
CouponPage = () => {
const { id: couponId } = useParams<{ id: string }>();
const client = getGraphQLClient();
const [data, setData] = useState<TCoupon | null>(null);
const [pickedCategories, setPickedCategories] = useState<TProductCategory[] | null>(null);
const [pickedProducts, setPickedProducts] = useState<TProduct[] | null>(null);
const [notFound, setNotFound] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [couponLoading, setCouponLoading] = useState<boolean>(false);
const history = useHistory();
const [canValidate, setCanValidate] = useState(false);
const getCouponData = async (id: number) => {
let couponData: TCoupon;
try {
couponData = await client.getCouponById(id,
gql`
fragment AdminPanelCouponFragment on Coupon {
id
createDate
updateDate
pageTitle
pageDescription
meta {
keywords
}
isEnabled
discountType
value
code
description
allowFreeShipping
minimumSpend
maximumSpend
categoryIds
productIds
expiryDate
usageLimit
customMeta (keys: ${JSON.stringify(getCustomMetaKeysFor(EDBEntity.Coupon))})
}`, 'AdminPanelCouponFragment');
if (couponData) {
setData(couponData);
}
} catch (e) {
console.error(e)
}
if (!couponData) {
setNotFound(true);
}
return couponData;
}
const init = async () => {
setCouponLoading(true);
let couponData: TCoupon;
if (couponId && couponId !== 'new') {
couponData = await getCouponData(parseInt(couponId));
}
if (couponId === 'new') {
setData({} as any);
}
setCouponLoading(false);
if (couponData?.categoryIds?.length) {
const categories = await Promise.all(couponData.categoryIds.map(async id => {
try {
return await client.getProductCategoryById(Number(id));
} catch (error) {
console.error(error);
}
}));
setPickedCategories(categories ?? []);
} else setPickedCategories([]);
if (couponData?.productIds?.length) {
const products = await Promise.all(couponData.productIds.map(async id => {
try {
return await client.getProductById(Number(id));
} catch (error) {
console.error(error);
}
}));
setPickedProducts(products ?? []);
} else setPickedProducts([]);
}
useEffect(() => {
init();
}, []);
const handleSave = async () => {
setCanValidate(true);
if (!data?.code || !data.value || !data.discountType) return;
setIsSaving(true);
const inputData: TCouponInput = {
slug: data.slug,
pageTitle: data.pageTitle,
pageDescription: data.pageDescription,
meta: data.meta && {
keywords: data.meta.keywords,
},
isEnabled: data.isEnabled,
discountType: data.discountType,
value: data.value,
code: data.code,
description: data.description,
allowFreeShipping: data.allowFreeShipping,
minimumSpend: data.minimumSpend ? parseFloat(data.minimumSpend as any) : null,
maximumSpend: data.maximumSpend ? parseFloat(data.maximumSpend as any) : null,
categoryIds: data.categoryIds,
productIds: data.productIds,
expiryDate: data.expiryDate,
usageLimit: data.usageLimit,
customMeta: Object.assign({}, data.customMeta, await getCustomMetaFor(EDBEntity.Coupon)),
}
if (couponId === 'new') {
try {
const newData = await client?.createCoupon(inputData);
toast.success('Created coupon!');
history.replace(`${couponPageInfo.baseRoute}/${newData.id}`)
await getCouponData(newData.id);
} catch (e) {
toast.error('Failed to create coupon');
console.error(e);
}
} else {
try {
await client?.updateCoupon(data.id, inputData);
await getCouponData(data.id);
toast.success('Saved!');
} catch (e) {
toast.error('Failed to save');
console.error(e)
}
}
setIsSaving(false);
setCanValidate(false);
}
const handleInputChange = (prop: keyof TCoupon, val: any) => {
if (data) {
setData((prevData) => {
const newData = Object.assign({}, prevData);
(newData[prop] as any) = val;
return newData;
});
}
}
const handleGenerateCode = () => {
handleInputChange('code', getRandStr(8).toUpperCase());
}
const handleSearchCategory = async (text: string, params: TPagedParams<TProductCategory>) => {
return client?.getFilteredProductCategories({
filterParams: {
nameSearch: text
},
pagedParams: params
});
}
const handleSearchProduct = async (text: string, params: TPagedParams<TProduct>) => {
return client?.getFilteredProducts({
filterParams: {
nameSearch: text
},
pagedParams: params
});
}
const refetchMeta = async () => {
if (!couponId) return;
const data = await getCouponData(parseInt(couponId));
return data?.customMeta;
};
if (notFound) {
return (
<div className={styles.CouponPage}>
<div className={styles.notFoundPage}>
<p className={styles.notFoundText}>Coupon not found</p>
</div>
</div>
)
}
let pageFullUrl;
if (data) {
pageFullUrl = serviceLocator.getFrontendUrl() + resolvePageRoute('coupon', { slug: data.slug ?? data.id + '' });
}
return (
<div className={styles.CouponPage}>
<div className={styles.header}>
<div className={styles.headerLeft}>
<IconButton
onClick={() => window.history.back()}
>
<ArrowBackIcon style={{ fontSize: '18px' }} />
</IconButton>
<p className={commonStyles.pageTitle}>coupon</p>
</div>
<div className={styles.headerActions}>
{pageFullUrl && (
<Tooltip title="Open coupon in the new tab">
<IconButton
style={{ marginRight: '10px' }}
className={styles.openPageBtn}
aria-label="open"
onClick={() => { window.open(pageFullUrl, '_blank'); }}
>
<OpenInNewIcon />
</IconButton>
</Tooltip>
)}
<Button variant="contained" color="primary"
className={styles.saveBtn}
size="small"
disabled={isSaving}
onClick={handleSave}>Save</Button>
</div>
</div>
<div className={styles.fields}>
{couponLoading && (
Array(8).fill(1).map((it, index) => (
<Skeleton style={{ marginBottom: '10px' }} key={index} height={"50px"} />
))
)}
{!couponLoading && (
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<TextField label="Code"
value={data?.code || ''}
fullWidth
variant="standard"
className={styles.textField}
onChange={(e) => { handleInputChange('code', e.target.value) }}
error={canValidate && !data?.code}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Tooltip title="Generate code">
<IconButton
aria-label="Generate code"
onClick={handleGenerateCode}
edge="end"
>
{<SmartButtonIcon />}
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} sm={6}></Grid>
<Grid item xs={12} sm={6}>
<Select
fullWidth
variant="standard"
label="Discount type"
value={(data?.discountType ?? '')}
onChange={(event: SelectChangeEvent<unknown>) => {
handleInputChange('discountType', event.target.value)
}}
error={canValidate && !data?.discountType}
options={[
{
value: 'fixed',
label: 'Fixed'
},
{
value: 'percentage',
label: 'Percentage'
}
]}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
label="Discount value"
className={styles.textField}
fullWidth
variant="standard"
value={data?.value || ''}
error={canValidate && !data?.value}
onChange={(e) => {
const val = Number(e.target.value);
if (!isNaN(val)) handleInputChange('value', val);
}}
/>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
label="Description"
className={styles.textField}
fullWidth
multiline
variant="standard"
value={data?.description || ''}
onChange={(e) => { handleInputChange('description', e.target.value) }}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
label="Cart total minimum"
className={styles.textField}
fullWidth
multiline
variant="standard"
value={data?.minimumSpend || ''}
onChange={(e) => { handleInputChange('minimumSpend', e.target.value) }}
InputProps={{
inputComponent: NumberFormatCustom as any,
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
label="Cart total maximum"
className={styles.textField}
fullWidth
multiline
variant="standard"
value={data?.maximumSpend || ''}
onChange={(e) => { handleInputChange('maximumSpend', e.target.value) }}
InputProps={{
inputComponent: NumberFormatCustom as any,
}}
/>
</Grid>
<Grid item xs={12} sm={6}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
label="Expiry date"
value={data?.expiryDate}
onChange={(newValue) => {
if (!newValue) {
handleInputChange('expiryDate', null);
return;
}
const date = new Date(newValue);
if (isNaN(date.getTime())) {
handleInputChange('expiryDate', null);
return;
}
handleInputChange('expiryDate', date);
}}
renderInput={(params) => <TextField
variant="standard"
fullWidth
{...params}
/>}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
label="Usage limit"
className={styles.textField}
fullWidth
variant="standard"
value={data?.usageLimit || ''}
onChange={(e) => {
const val = Number(e.target.value);
if (!isNaN(val)) handleInputChange('usageLimit', val);
}}
/>
</Grid>
<Grid item xs={12} sm={12}>
{pickedCategories ? (
<Autocomplete<TProductCategory>
multiple
loader={handleSearchCategory}
onSelect={(data: TProductCategory[]) => {
if (!data?.length) handleInputChange('categoryIds', null);
else handleInputChange('categoryIds', data.map(cat => cat.id));
}}
getOptionLabel={(data) => `${data.name} (id: ${data.id}${data?.parent?.id ? `; parent id: ${data.parent.id}` : ''})`}
getOptionValue={(data) => data.name}
fullWidth
className={styles.textField}
defaultValue={pickedCategories}
label={"Categories"}
/>
) : <LoadBox size={30} />}
</Grid>
<Grid item xs={12} sm={12}>
{pickedProducts ? (
<Autocomplete<TProduct>
multiple
loader={handleSearchProduct}
onSelect={(data: TProduct[]) => {
if (!data?.length) handleInputChange('productIds', null);
else handleInputChange('productIds', data.map(cat => cat.id));
}}
getOptionLabel={(data) => `${data.name} (id: ${data.id})`}
getOptionValue={(data) => data.name}
fullWidth
className={styles.textField}
defaultValue={pickedProducts}
label={"Products"}
/>
) : <LoadBox size={30} />}
</Grid>
<Grid item xs={12} >
{data && (
<RenderCustomFields
entityType={EDBEntity.Coupon}
entityData={data}
refetchMeta={refetchMeta}
/>
)}
</Grid>
</Grid>
)}
</div>
</div>
)
}
Example #19
Source File: QueryStateEditor.tsx From mui-toolpad with MIT License | 4 votes |
function QueryStateNodeEditor<P>({ node }: QueryStateNodeEditorProps<P>) {
const dom = useDom();
const domApi = useDomApi();
const app = appDom.getApp(dom);
const { apis = [] } = appDom.getChildNodes(dom, app);
const handleSelectionChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const apiNodeId = event.target.value ? (event.target.value as NodeId) : null;
domApi.setNodeNamespacedProp(node, 'attributes', 'api', appDom.createConst(apiNodeId));
},
[domApi, node],
);
const handleRefetchOnWindowFocusChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
domApi.setNodeNamespacedProp(
node,
'attributes',
'refetchOnWindowFocus',
appDom.createConst(event.target.checked),
);
},
[domApi, node],
);
const handleRefetchOnReconnectChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
domApi.setNodeNamespacedProp(
node,
'attributes',
'refetchOnReconnect',
appDom.createConst(event.target.checked),
);
},
[domApi, node],
);
const handleRefetchIntervalChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const interval = Number(event.target.value);
if (Number.isNaN(interval) || interval <= 0) {
domApi.setNodeNamespacedProp(node, 'attributes', 'refetchInterval', undefined);
} else {
domApi.setNodeNamespacedProp(
node,
'attributes',
'refetchInterval',
appDom.createConst(interval * 1000),
);
}
},
[domApi, node],
);
const argTypes = getQueryNodeArgTypes(dom, node);
return (
<React.Fragment>
<Stack spacing={1} py={1}>
<NodeNameEditor node={node} />
<TextField
select
fullWidth
value={node.attributes.api.value || ''}
label="Query"
onChange={handleSelectionChange}
>
<MenuItem value="">---</MenuItem>
{apis.map(({ id, name }) => (
<MenuItem key={id} value={id}>
{name}
</MenuItem>
))}
</TextField>
<ParamsEditor node={node} argTypes={argTypes} />
<FormControlLabel
control={
<Checkbox
checked={node.attributes.refetchOnWindowFocus?.value ?? true}
onChange={handleRefetchOnWindowFocusChange}
/>
}
label="Refetch on window focus"
/>
<FormControlLabel
control={
<Checkbox
checked={node.attributes.refetchOnReconnect?.value ?? true}
onChange={handleRefetchOnReconnectChange}
/>
}
label="Refetch on network reconnect"
/>
<TextField
InputProps={{
startAdornment: <InputAdornment position="start">s</InputAdornment>,
}}
sx={{ maxWidth: 300 }}
type="number"
label="Refetch interval"
value={refetchIntervalInSeconds(node.attributes.refetchInterval?.value) ?? ''}
onChange={handleRefetchIntervalChange}
/>
<PreviewQueryStateResult node={node} />
</Stack>
</React.Fragment>
);
}
Example #20
Source File: QueryEditor.tsx From mui-toolpad with MIT License | 4 votes |
function QueryNodeEditorDialog<Q, P>({
open,
node,
onClose,
onRemove,
onSave,
}: QueryNodeEditorProps<Q, P>) {
const { appId } = usePageEditorState();
const [input, setInput] = React.useState(node);
React.useEffect(() => setInput(node), [node]);
const connectionId = input.attributes.connectionId.value;
const dataSourceId = input.attributes.dataSource?.value;
const dataSource = (dataSourceId && dataSources[dataSourceId]) || null;
const handleConnectionChange = React.useCallback((newConnectionId) => {
setInput((existing) =>
update(existing, {
attributes: update(existing.attributes, {
connectionId: appDom.createConst(newConnectionId),
}),
}),
);
}, []);
const handleQueryChange = React.useCallback((newQuery: Q) => {
setInput((existing) =>
update(existing, {
attributes: update(existing.attributes, {
query: appDom.createConst(newQuery),
}),
}),
);
}, []);
const handleTransformFnChange = React.useCallback((newValue: string) => {
setInput((existing) =>
update(existing, {
attributes: update(existing.attributes, {
transform: appDom.createConst(newValue),
}),
}),
);
}, []);
const handleTransformEnabledChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setInput((existing) =>
update(existing, {
attributes: update(existing.attributes, {
transformEnabled: appDom.createConst(event.target.checked),
}),
}),
);
},
[],
);
const handleRefetchOnWindowFocusChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setInput((existing) =>
update(existing, {
attributes: update(existing.attributes, {
refetchOnWindowFocus: appDom.createConst(event.target.checked),
}),
}),
);
},
[],
);
const handleRefetchOnReconnectChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setInput((existing) =>
update(existing, {
attributes: update(existing.attributes, {
refetchOnReconnect: appDom.createConst(event.target.checked),
}),
}),
);
},
[],
);
const handleRefetchIntervalChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const interval = Number(event.target.value);
setInput((existing) =>
update(existing, {
attributes:
Number.isNaN(interval) || interval <= 0
? omit(existing.attributes, 'refetchInterval')
: update(existing.attributes, {
refetchInterval: appDom.createConst(interval * 1000),
}),
}),
);
},
[],
);
const [params, setParams] = React.useState<[string, BindableAttrValue<any>][]>(
Object.entries(input.params || {}),
);
React.useEffect(() => setParams(Object.entries(input.params || {})), [input.params]);
const { pageState } = usePageEditorState();
const liveParams: [string, LiveBinding][] = React.useMemo(() => {
return params.map(([name, bindable]) => [name, evaluateBindable(bindable, pageState)]);
}, [params, pageState]);
const handleParamsChange = React.useCallback((newParams: [string, BindableAttrValue<any>][]) => {
setParams(newParams);
const paramsObj: BindableAttrValues<any> = Object.fromEntries(newParams);
setInput((existing) =>
update(existing, {
params: paramsObj,
}),
);
}, []);
const handleSave = React.useCallback(() => {
onSave(input);
}, [onSave, input]);
const handleRemove = React.useCallback(() => {
onRemove(node);
onClose();
}, [onRemove, node, onClose]);
const paramsObject: Record<string, any> = React.useMemo(() => {
const liveParamValues: [string, any][] = liveParams.map(([name, result]) => [
name,
result?.value,
]);
return Object.fromEntries(liveParamValues);
}, [liveParams]);
const [previewQuery, setPreviewQuery] = React.useState<appDom.QueryNode<Q, P> | null>(null);
const [previewParams, setPreviewParams] = React.useState(paramsObject);
const queryPreview = client.useQuery(
'execQuery',
previewQuery ? [appId, previewQuery, previewParams] : null,
{ retry: false },
);
const handleUpdatePreview = React.useCallback(() => {
setPreviewQuery(input);
setPreviewParams(paramsObject);
}, [input, paramsObject]);
const isInputSaved = node === input;
const handleClose = React.useCallback(() => {
const ok = isInputSaved
? true
: // eslint-disable-next-line no-alert
window.confirm(
'Are you sure you want to close the editor. All unsaved progress will be lost.',
);
if (ok) {
onClose();
}
}, [onClose, isInputSaved]);
if (!dataSourceId || !dataSource) {
throw new Error(`DataSource "${dataSourceId}" not found`);
}
return (
<Dialog fullWidth maxWidth="lg" open={open} onClose={handleClose} scroll="body">
<DialogTitle>Edit Query ({node.id})</DialogTitle>
<DialogContent>
<Stack spacing={1} py={1} gap={2}>
<Stack direction="row" gap={2}>
<NodeNameEditor node={node} />
<ConnectionSelect
dataSource={dataSourceId}
value={input.attributes.connectionId.value || null}
onChange={handleConnectionChange}
/>
</Stack>
<Divider />
<Typography>Parameters</Typography>
<ParametersEditor
value={params}
onChange={handleParamsChange}
globalScope={pageState}
liveValue={liveParams}
/>
<Divider />
<Typography>Build query:</Typography>
<dataSource.QueryEditor
appId={appId}
connectionId={connectionId}
value={input.attributes.query.value}
onChange={handleQueryChange}
globalScope={{ query: paramsObject }}
/>
<Divider />
<Typography>Options:</Typography>
<Grid container direction="row" spacing={1}>
<Grid item xs={4}>
<Stack direction="column">
<FormControlLabel
control={
<Checkbox
checked={input.attributes.refetchOnWindowFocus?.value ?? true}
onChange={handleRefetchOnWindowFocusChange}
/>
}
label="Refetch on window focus"
/>
<FormControlLabel
control={
<Checkbox
checked={input.attributes.refetchOnReconnect?.value ?? true}
onChange={handleRefetchOnReconnectChange}
/>
}
label="Refetch on network reconnect"
/>
<TextField
InputProps={{
startAdornment: <InputAdornment position="start">s</InputAdornment>,
}}
sx={{ maxWidth: 300 }}
type="number"
label="Refetch interval"
value={refetchIntervalInSeconds(input.attributes.refetchInterval?.value) ?? ''}
onChange={handleRefetchIntervalChange}
/>
</Stack>
</Grid>
<Grid item xs={6}>
<Stack>
<FormControlLabel
label="Transform API response"
control={
<Checkbox
checked={input.attributes.transformEnabled?.value ?? false}
onChange={handleTransformEnabledChange}
inputProps={{ 'aria-label': 'controlled' }}
/>
}
/>
<JsExpressionEditor
globalScope={{}}
value={input.attributes.transform?.value ?? '(data) => {\n return data;\n}'}
onChange={handleTransformFnChange}
disabled={!input.attributes.transformEnabled?.value}
/>
</Stack>
</Grid>
</Grid>
<Divider />
<Toolbar disableGutters>
preview
<LoadingButton
sx={{ ml: 2 }}
disabled={previewParams === paramsObject && previewQuery === input}
loading={queryPreview.isLoading}
loadingPosition="start"
onClick={handleUpdatePreview}
startIcon={<PlayArrowIcon />}
>
Run
</LoadingButton>
</Toolbar>
{queryPreview.error ? <ErrorAlert error={queryPreview.error} /> : null}
{queryPreview.isSuccess ? <JsonView src={queryPreview.data} /> : null}
</Stack>
</DialogContent>
<DialogActions>
<Button color="inherit" variant="text" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleRemove}>Remove</Button>
<Button disabled={isInputSaved} onClick={handleSave}>
Save
</Button>
</DialogActions>
</Dialog>
);
}
Example #21
Source File: Welcome.tsx From Cromwell with MIT License | 4 votes |
export default function WelcomePage() {
const apiClient = getRestApiClient();
const graphQLClient = getGraphQLClient();
const history = useHistory();
const [showPassword, setShowPassword] = useState(false);
const [submitPressed, setSubmitPressed] = useState(false);
const [emailInput, setEmailInput] = useState('');
const [passwordInput, setPasswordInput] = useState('');
const [nameInput, setNameInput] = useState('');
const [avatarInput, setAvatarInput] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
}
const handleSubmitClick = async () => {
setSubmitPressed(true);
if (!emailInput || !passwordInput || !nameInput) {
return;
}
setLoading(true);
try {
await graphQLClient.createUser({
fullName: nameInput,
email: emailInput,
password: passwordInput,
avatar: avatarInput,
role: 'administrator',
});
} catch (e) {
toast.error('Failed to create user with provided credentials');
console.error(e);
setLoading(false);
return;
}
try {
await apiClient.setUpCms({
url: window.location.origin,
});
await apiClient.login({
email: emailInput,
password: passwordInput
});
} catch (e) {
console.error(e);
}
checkAuth();
setLoading(false);
}
const checkAuth = async () => {
const userInfo = await apiClient.getUserInfo({ disableLog: true });
if (userInfo) {
setStoreItem('userInfo', userInfo);
history?.push?.(`/`);
}
}
return (
<div className={styles.WelcomePage}>
<div className={styles.wrapper}>
<img src="/admin/static/logo_small_black.svg" width="100px" className={styles.logo} />
<h1 className={styles.title}>Welcome to Cromwell CMS!</h1>
<h3 className={styles.subtitle}>Let's create your account</h3>
<div className={styles.inputForm}>
<div className={styles.userMainInfo}>
<ImagePicker
toolTip="Pick avatar"
onChange={setAvatarInput}
value={avatarInput}
className={styles.avatar}
hideSrc
/>
<CssTextField
label="Name"
value={nameInput}
onChange={e => setNameInput(e.target.value)}
fullWidth
variant="standard"
error={nameInput === '' && submitPressed}
helperText={nameInput === '' && submitPressed ? "This field is required" : undefined}
id="name-input"
/>
</div>
<CssTextField
label="E-mail"
value={emailInput}
className={styles.textField}
onChange={e => setEmailInput(e.target.value)}
fullWidth
variant="standard"
error={emailInput === '' && submitPressed}
helperText={emailInput === '' && submitPressed ? "This field is required" : undefined}
id="email-input"
/>
<CssTextField
label="Password"
type={showPassword ? 'text' : 'password'}
value={passwordInput}
onChange={e => setPasswordInput(e.target.value)}
className={styles.textField}
fullWidth
variant="standard"
error={passwordInput === '' && submitPressed}
helperText={passwordInput === '' && submitPressed ? "This field is required" : undefined}
id="password-input"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
),
}}
/>
<Button
onClick={handleSubmitClick}
className={styles.createBtn}
disabled={loading}
color="primary"
variant="contained"
>Create</Button>
</div>
</div>
<LoadingStatus isActive={loading} />
</div>
);
}
Example #22
Source File: VolumesSummary.tsx From console with GNU Affero General Public License v3.0 | 4 votes |
TenantVolumes = ({ classes, history, match }: ITenantVolumesProps) => {
const dispatch = useDispatch();
const loadingTenant = useSelector(
(state: AppState) => state.tenants.loadingTenant
);
const [records, setRecords] = useState<IStoragePVCs[]>([]);
const [filter, setFilter] = useState("");
const [loading, setLoading] = useState<boolean>(true);
const [selectedPVC, setSelectedPVC] = useState<any>(null);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"];
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/pvcs`
)
.then((res: IStoragePVCs) => {
let volumes = get(res, "pvcs", []);
setRecords(volumes ? volumes : []);
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
}
}, [loading, dispatch, tenantName, tenantNamespace]);
const confirmDeletePVC = (pvcItem: IStoragePVCs) => {
const delPvc = {
...pvcItem,
tenant: tenantName,
namespace: tenantNamespace,
};
setSelectedPVC(delPvc);
setDeleteOpen(true);
};
const filteredRecords: IStoragePVCs[] = records.filter((elementItem) =>
elementItem.name.toLowerCase().includes(filter.toLowerCase())
);
const PVCViewAction = (PVC: IPodListElement) => {
history.push(
`/namespaces/${tenantNamespace}/tenants/${tenantName}/pvcs/${PVC.name}`
);
return;
};
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
setLoading(true);
};
useEffect(() => {
if (loadingTenant) {
setLoading(true);
}
}, [loadingTenant]);
return (
<Fragment>
{deleteOpen && (
<DeletePVC
deleteOpen={deleteOpen}
selectedPVC={selectedPVC}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Grid container spacing={1}>
<h1 className={classes.sectionTitle}>Volumes</h1>
<Grid item xs={12}>
<TextField
placeholder="Search Volumes (PVCs)"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
variant="standard"
/>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
itemActions={[
{ type: "view", onClick: PVCViewAction },
{ type: "delete", onClick: confirmDeletePVC },
]}
columns={[
{
label: "Name",
elementKey: "name",
},
{
label: "Status",
elementKey: "status",
width: 120,
},
{
label: "Capacity",
elementKey: "capacity",
width: 120,
},
{
label: "Storage Class",
elementKey: "storageClass",
},
]}
isLoading={loading}
records={filteredRecords}
entityName="PVCs"
idField="name"
customPaperHeight={classes.tableWrapper}
/>
</Grid>
</Grid>
</Fragment>
);
}
Example #23
Source File: PodsSummary.tsx From console with GNU Affero General Public License v3.0 | 4 votes |
PodsSummary = ({ classes, match, history }: IPodsSummary) => {
const dispatch = useDispatch();
const loadingTenant = useSelector(
(state: AppState) => state.tenants.loadingTenant
);
const [pods, setPods] = useState<IPodListElement[]>([]);
const [loadingPods, setLoadingPods] = useState<boolean>(true);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedPod, setSelectedPod] = useState<any>(null);
const [filter, setFilter] = useState("");
const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"];
const podViewAction = (pod: IPodListElement) => {
history.push(
`/namespaces/${tenantNamespace}/tenants/${tenantName}/pods/${pod.name}`
);
return;
};
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
setLoadingPods(true);
};
const confirmDeletePod = (pod: IPodListElement) => {
pod.tenant = tenantName;
pod.namespace = tenantNamespace;
setSelectedPod(pod);
setDeleteOpen(true);
};
const filteredRecords: IPodListElement[] = pods.filter((elementItem) =>
elementItem.name.toLowerCase().includes(filter.toLowerCase())
);
const podTableActions = [
{ type: "view", onClick: podViewAction },
{ type: "delete", onClick: confirmDeletePod },
];
useEffect(() => {
if (loadingTenant) {
setLoadingPods(true);
}
}, [loadingTenant]);
useEffect(() => {
if (loadingPods) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/pods`
)
.then((result: IPodListElement[]) => {
for (let i = 0; i < result.length; i++) {
let currentTime = (Date.now() / 1000) | 0;
result[i].time = niceDays(
(currentTime - parseInt(result[i].timeCreated)).toString()
);
}
setPods(result);
setLoadingPods(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(
setErrorSnackMessage({
errorMessage: "Error loading pods",
detailedError: err.detailedError,
})
);
});
}
}, [loadingPods, tenantName, tenantNamespace, dispatch]);
return (
<Fragment>
{deleteOpen && (
<DeletePod
deleteOpen={deleteOpen}
selectedPod={selectedPod}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<h1 className={classes.sectionTitle}>Pods</h1>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Pods"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
variant="standard"
/>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
columns={[
{ label: "Name", elementKey: "name", width: 200 },
{ label: "Status", elementKey: "status" },
{ label: "Age", elementKey: "time" },
{ label: "Pod IP", elementKey: "podIP" },
{
label: "Restarts",
elementKey: "restarts",
renderFunction: (input) => {
return input !== null ? input : 0;
},
},
{ label: "Node", elementKey: "node" },
]}
isLoading={loadingPods}
records={filteredRecords}
itemActions={podTableActions}
entityName="Pods"
idField="name"
/>
</Grid>
</Fragment>
);
}
Example #24
Source File: LoginPage.tsx From Cromwell with MIT License | 4 votes |
LoginPage = () => {
const apiClient = getRestApiClient();
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [canValidate, setCanValidate] = useState(false);
const [emailInput, setEmailInput] = useState('');
const [codeInput, setCodeInput] = useState('');
const [passwordInput, setPasswordInput] = useState('');
const history = useHistory();
const [formType, setFormType] = useState<TFromType>('sign-in');
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
}
const handleLoginClick = async (event: TEvent) => {
event?.preventDefault?.();
setCanValidate(true);
if (!emailInput || !passwordInput) return;
setLoading(true);
try {
await apiClient.login({
email: emailInput,
password: passwordInput
}, { disableLog: true });
checkAuth(true);
} catch (e) {
if (e.statusCode === 0) {
toast.error('Could not connect to the Server');
} else {
toast.error('Incorrect email or password');
}
console.error(e);
}
setLoading(false);
}
const checkAuth = async (showError?: boolean) => {
const userInfo = await apiClient.getUserInfo({ disableLog: true });
if (userInfo?.id) {
if (!userInfo.role || !userInfo.email) {
if (showError) toast.error('Incorrect user account');
return;
}
setStoreItem('userInfo', userInfo);
loginSuccess(userInfo);
} else {
if (showError) toast.error('Incorrect email or password');
}
}
useEffect(() => {
checkAuth();
}, []);
const loginSuccess = (userInfo: TUser) => {
if (userInfo.role === 'administrator' || userInfo.role === 'guest') {
history?.push?.(`/`);
} else if (userInfo.role === 'author') {
history?.push?.(`/post-list`);
} else {
toast.error('Access forbidden');
}
}
const handleSubmit = (event: TEvent) => {
setCanValidate(true);
event?.preventDefault?.();
if (formType === 'sign-in') handleLoginClick(event);
if (formType === 'forgot-pass') handleForgotPass();
if (formType === 'reset-pass') handleResetPass();
}
const handleGoToForgotPass = () => {
setFormType('forgot-pass');
}
const handleForgotPass = async () => {
if (!emailInput || emailInput == '') return;
setLoading(true);
try {
const success = await apiClient?.forgotPassword({ email: emailInput }, { disableLog: true });
if (success) {
toast.success('We sent you an e-mail');
setFormType('reset-pass');
} else {
throw new Error('!success');
}
} catch (e) {
console.error(e);
let info = e?.message;
try {
info = JSON.parse(e.message)
} catch (e) { }
if (info?.statusCode === 429) {
} else {
toast.error?.('Incorrect e-mail or user was not found');
}
}
setLoading(false);
}
const handleResetPass = async () => {
if (!emailInput || !codeInput || !passwordInput) return;
setLoading(true);
try {
const success = await apiClient?.resetPassword({
email: emailInput,
code: codeInput,
newPassword: passwordInput,
});
if (success) {
toast.success('Password has been changed.');
setFormType('sign-in');
} else {
throw new Error('!success');
}
} catch (e) {
console.error(e);
let info = e?.message;
try {
info = JSON.parse(e.message)
} catch (e) { }
if (info?.statusCode === 429) {
} else if (info?.statusCode === 417) {
toast.error?.('Exceeded reset password attempts');
setCodeInput('')
setFormType('sign-in');
} else {
toast.error?.('Incorrect e-mail or user was not found');
}
}
setLoading(false);
}
return (
<div className={styles.LoginPage}>
<form className={styles.loginForm} onSubmit={handleSubmit}>
<img src="/admin/static/logo_small_black.svg" width="100px" className={styles.logo} />
{formType === 'sign-in' && (
<>
<TextField
label="E-mail"
value={emailInput}
className={styles.textField}
onChange={e => setEmailInput(e.target.value)}
fullWidth
variant="standard"
id="email-input"
error={canValidate && !emailInput}
/>
<TextField
label="Password"
type={showPassword ? 'text' : 'password'}
value={passwordInput}
onChange={e => setPasswordInput(e.target.value)}
className={styles.textField}
fullWidth
id="password-input"
variant="standard"
error={canValidate && !passwordInput}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
),
}}
/>
<Button
type="submit"
onClick={handleLoginClick}
className={styles.loginBtn}
disabled={loading}
variant="outlined"
color="inherit">Login</Button>
<p onClick={handleGoToForgotPass} className={styles.forgotPassText}>Forgot password?</p>
</>
)}
{formType === 'forgot-pass' && (
<>
<TextField
label="E-mail"
value={emailInput}
className={styles.textField}
onChange={e => setEmailInput(e.target.value)}
fullWidth
variant="standard"
id="email-input"
error={canValidate && !passwordInput}
/>
<Button
type="submit"
onClick={handleForgotPass}
className={styles.loginBtn}
disabled={loading}
variant="outlined"
color="inherit">Reset password</Button>
<p onClick={() => setFormType('sign-in')} className={styles.forgotPassText}>back</p>
</>
)}
{formType === 'reset-pass' && (
<>
<p className={styles.resetPassInstructions}>We sent you an e-mail with reset code. Copy the code below and create a new password</p>
<TextField
label="Code"
value={codeInput}
className={styles.textField}
onChange={e => setCodeInput(e.target.value)}
fullWidth
variant="standard"
id="code-input"
error={canValidate && !codeInput}
/>
<TextField
label="New password"
type={showPassword ? 'text' : 'password'}
value={passwordInput}
onChange={e => setPasswordInput(e.target.value)}
className={styles.textField}
fullWidth
id="password-input"
error={canValidate && !passwordInput}
variant="standard"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
),
}}
/>
<Button
type="submit"
onClick={handleResetPass}
className={styles.loginBtn}
disabled={loading}
variant="outlined"
color="inherit">Change password</Button>
</>
)}
</form>
</div>
)
}
Example #25
Source File: User.tsx From Cromwell with MIT License | 4 votes |
export default function UserPage() {
const { id: userId } = useParams<{ id: string }>();
const client = getGraphQLClient();
const [notFound, setNotFound] = useState(false);
const [passwordInput, setPasswordInput] = useState('');
const history = useHistory();
const [userData, setUserData] = useState<TUser | undefined | null>(null);
const [showPassword, setShowPassword] = useState(false);
const isNew = userId === 'new';
const [canValidate, setCanValidate] = useState(false);
// Support old and new address format
const { addressString, addressJson } = parseAddress(userData?.address);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
}
const getUser = async (id: number) => {
let data: TUser | undefined;
try {
data = await client?.getUserById(id,
gql`
fragment AdminPanelUserFragment on User {
id
slug
createDate
updateDate
isEnabled
pageTitle
pageDescription
meta {
keywords
}
fullName
email
avatar
bio
phone
address
role
customMeta (keys: ${JSON.stringify(getCustomMetaKeysFor(EDBEntity.User))})
}`, 'AdminPanelUserFragment');
} catch (e) { console.error(e) }
return data;
}
const init = async () => {
if (userId && !isNew) {
const data = await getUser(parseInt(userId));
if (data?.id) {
setUserData(data);
} else setNotFound(true);
} else if (isNew) {
setUserData({} as any);
}
}
useEffect(() => {
init();
}, []);
const refetchMeta = async () => {
if (!userId) return;
const data = await getUser(parseInt(userId));
return data?.customMeta;
};
const handleInputChange = (prop: keyof TUser, val: any) => {
if (userData) {
setUserData((prevData) => {
const newData = Object.assign({}, prevData);
(newData[prop] as any) = val;
return newData;
});
}
}
const getInput = async (): Promise<TUpdateUser> => ({
slug: userData.slug,
pageTitle: userData.pageTitle,
pageDescription: userData.pageDescription,
fullName: userData.fullName,
email: userData.email,
avatar: userData.avatar,
bio: userData.bio,
phone: userData.phone,
address: userData.address,
role: userData.role,
customMeta: Object.assign({}, userData.customMeta, await getCustomMetaFor(EDBEntity.User)),
});
const handleSave = async () => {
setCanValidate(true);
const inputData = await getInput();
if (!inputData.email || !inputData.fullName || !inputData.role) return;
if (isNew) {
if (!passwordInput) return;
try {
const createInput: TCreateUser = {
...inputData,
password: passwordInput
}
const newData = await client?.createUser(createInput);
toast.success('Created user');
history.replace(`${userPageInfo.baseRoute}/${newData.id}`);
setUserData(newData);
} catch (e) {
toast.error('Failed to create user');
console.error(e);
}
} else if (userData?.id) {
try {
await client?.updateUser(userData.id, inputData);
const newData = await getUser(parseInt(userId));
setUserData(newData);
toast.success('Saved!');
const currentUser: TUser | undefined = getStoreItem('userInfo');
if (currentUser?.id && currentUser.id === newData.id) {
setStoreItem('userInfo', userData);
}
} catch (e) {
toast.error('Failed to save');
console.error(e);
}
}
setCanValidate(false);
}
if (notFound) {
return (
<div className={styles.UserPage}>
<div className={styles.notFoundPage}>
<p className={styles.notFoundText}>User not found</p>
</div>
</div>
)
}
return (
<div className={styles.UserPage}>
<div className={styles.header}>
<div className={styles.headerLeft}>
<IconButton
onClick={() => window.history.back()}
>
<ArrowBackIcon style={{ fontSize: '18px' }} />
</IconButton>
<p className={commonStyles.pageTitle}>account</p>
</div>
<div className={styles.headerActions}>
<Button variant="contained" color="primary"
className={styles.saveBtn}
size="small"
onClick={handleSave}>
Save</Button>
</div>
</div>
<div className={styles.fields}>
<Grid container spacing={3}>
<Grid item xs={12} sm={6} style={{ display: 'flex', alignItems: 'flex-end' }}>
<TextField
label="Name"
value={userData?.fullName || ''}
fullWidth
variant="standard"
className={styles.field}
onChange={(e) => { handleInputChange('fullName', e.target.value) }}
error={canValidate && !userData?.fullName}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ImagePicker
label="Avatar"
onChange={(val) => { handleInputChange('avatar', val) }}
value={userData?.avatar ?? null}
className={styles.imageField}
classes={{ image: styles.image }}
backgroundSize='cover'
width="50px"
height="50px"
showRemove
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
label="E-mail"
value={userData?.email || ''}
fullWidth
variant="standard"
className={styles.field}
onChange={(e) => { handleInputChange('email', e.target.value) }}
error={canValidate && !userData?.email}
/>
</Grid>
{isNew && (
<Grid item xs={12} sm={6}>
<TextField
label="Password"
value={passwordInput || ''}
type={showPassword ? 'text' : 'password'}
fullWidth
variant="standard"
className={styles.field}
onChange={(e) => { setPasswordInput(e.target.value) }}
error={canValidate && !passwordInput}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
)}
<Grid item xs={12} sm={6} display="flex" alignItems="flex-end">
<Select
fullWidth
variant="standard"
label="Role"
value={(userData?.role ?? '') as TUserRole}
onChange={(event: SelectChangeEvent<unknown>) => {
handleInputChange('role', event.target.value)
}}
error={canValidate && !userData?.role}
options={userRoles.map(role => ({ label: role, value: role }))}
/>
</Grid>
<Grid item xs={12} sm={12}>
<TextField
label="Bio"
value={userData?.bio || ''}
fullWidth
variant="standard"
multiline
className={styles.field}
onChange={(e) => { handleInputChange('bio', e.target.value) }}
/>
</Grid>
{!addressJson && (
<Grid item xs={12} sm={12}>
<TextField label="Address"
value={addressString || ''}
fullWidth
variant="standard"
className={styles.field}
onChange={(e) => { handleInputChange('address', e.target.value) }}
/>
</Grid>
)}
{addressJson && (
Object.entries<any>(addressJson).map(([fieldKey, value]) => {
return (
<Grid item xs={12} sm={6} key={fieldKey}>
<TextField label={fieldKey}
value={value || ''}
fullWidth
variant="standard"
className={styles.field}
onChange={(e) => {
const newVal = e.target.value;
handleInputChange('address', JSON.stringify({
...addressJson,
[fieldKey]: newVal,
}))
}}
/>
</Grid>
)
}))}
<Grid item xs={12} sm={6}>
<TextField
label="Phone"
value={userData?.phone || ''}
fullWidth
variant="standard"
className={styles.field}
onChange={(e) => { handleInputChange('phone', e.target.value) }}
/>
</Grid>
<Grid item xs={12} sm={12}>
{userData && (
<RenderCustomFields
entityType={EDBEntity.User}
entityData={userData}
refetchMeta={refetchMeta}
/>
)}
</Grid>
</Grid>
</div>
</div>
);
}
Example #26
Source File: AccountList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function AccountList({ group }) {
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const toggleSpeedDial = () => setSpeedDialOpen((currValue) => !currValue);
const [showPersonalAccountCreationModal, setShowPersonalAccountCreationModal] = useState(false);
const [showClearingAccountCreationModal, setShowClearingAccountCreationModal] = useState(false);
const [activeTab, setActiveTab] = useState("personal");
const [searchValuePersonal, setSearchValuePersonal] = useState("");
const [searchValueClearing, setSearchValueClearing] = useState("");
const [showPersonalAccountEditModal, setShowPersonalAccountEditModal] = useState(false);
const [showClearingAccountEditModal, setShowClearingAccountEditModal] = useState(false);
const [clearingAccountToCopy, setClearingAccountToCopy] = useState(undefined);
const [accountToEdit, setAccountToEdit] = useState(null);
const [clearingAccountToEdit, setClearingAccountToEdit] = useState(null);
const setAccounts = useSetRecoilState(groupAccounts(group.id));
const personalAccounts = useRecoilValue(personalAccountsSeenByUser(group.id));
const clearingAccounts = useRecoilValue(clearingAccountsSeenByUser(group.id));
const allAccounts = useRecoilValue(accountsSeenByUser(group.id));
const [accountToDelete, setAccountToDelete] = useState(null);
const userPermissions = useRecoilValue(currUserPermissions(group.id));
const currentUser = useRecoilValue(userData);
const memberIDToUsername = useRecoilValue(groupMemberIDsToUsername(group.id));
const [filteredPersonalAccounts, setFilteredPersonalAccounts] = useState([]);
const [filteredClearingAccounts, setFilteredClearingAccounts] = useState([]);
useEffect(() => {
if (searchValuePersonal != null && searchValuePersonal !== "") {
setFilteredPersonalAccounts(
personalAccounts.filter((t) => {
return (
t.name.toLowerCase().includes(searchValuePersonal.toLowerCase()) ||
t.description.toLowerCase().includes(searchValuePersonal.toLowerCase())
);
})
);
} else {
return setFilteredPersonalAccounts(personalAccounts);
}
}, [personalAccounts, searchValuePersonal, setFilteredPersonalAccounts]);
useEffect(() => {
if (searchValueClearing != null && searchValueClearing !== "") {
setFilteredClearingAccounts(
clearingAccounts.filter((t) => {
return (
t.name.toLowerCase().includes(searchValueClearing.toLowerCase()) ||
t.description.toLowerCase().includes(searchValueClearing.toLowerCase())
);
})
);
} else {
return setFilteredClearingAccounts(clearingAccounts);
}
}, [clearingAccounts, searchValueClearing, setFilteredClearingAccounts]);
useTitle(`${group.name} - Accounts`);
const openAccountEdit = (account) => {
setAccountToEdit(account);
setShowPersonalAccountEditModal(true);
};
const closeAccountEdit = (evt, reason) => {
if (reason !== "backdropClick") {
setShowPersonalAccountEditModal(false);
setAccountToEdit(null);
}
};
const openClearingAccountEdit = (account) => {
setClearingAccountToEdit(account);
setShowClearingAccountEditModal(true);
};
const closeClearingAccountEdit = (evt, reason) => {
if (reason !== "backdropClick") {
setShowClearingAccountEditModal(false);
setClearingAccountToEdit(null);
}
};
const confirmDeleteAccount = () => {
if (accountToDelete !== null) {
deleteAccount({ accountID: accountToDelete })
.then((account) => {
updateAccount(account, setAccounts);
setAccountToDelete(null);
})
.catch((err) => {
toast.error(err);
});
}
};
const openCreateDialog = () => {
setClearingAccountToCopy(undefined);
setShowClearingAccountCreationModal(true);
};
const copyClearingAccount = (account) => {
setClearingAccountToCopy(account);
setShowClearingAccountCreationModal(true);
};
return (
<>
<MobilePaper>
<TabContext value={activeTab}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList onChange={(e, newValue) => setActiveTab(newValue)} centered>
<Tab
value="personal"
label={
<TextBadge badgeContent={personalAccounts.length} color="primary">
<span>Personal Accounts</span>
</TextBadge>
}
/>
<Tab
label={
<TextBadge badgeContent={clearingAccounts.length} color="primary">
<span>Clearing Accounts</span>
</TextBadge>
}
value="clearing"
/>
</TabList>
</Box>
<TabPanel value="personal">
<List>
{personalAccounts.length === 0 ? (
<Alert severity="info">No Accounts</Alert>
) : (
<>
<ListItem>
<Input
value={searchValuePersonal}
onChange={(e) => setSearchValuePersonal(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValuePersonal("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
</ListItem>
<Divider />
{filteredPersonalAccounts.map((account) => (
<ListItem sx={{ padding: 0 }} key={account.id}>
<ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
<ListItemText
primary={
<div>
<span>{account.name}</span>
{account.owning_user_id === currentUser.id ? (
<span>
, owned by{" "}
<Chip
size="small"
component="span"
color="primary"
label="you"
/>
</span>
) : (
account.owning_user_id !== null && (
<span>
, owned by{" "}
<Chip
size="small"
component="span"
color="secondary"
label={
memberIDToUsername[
account.owning_user_id
]
}
/>
</span>
)
)}
</div>
}
secondary={account.description}
/>
</ListItemLink>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() => openAccountEdit(account)}
>
<Edit />
</IconButton>
<IconButton
color="error"
onClick={() => setAccountToDelete(account.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</>
)}
</List>
{userPermissions.can_write && (
<>
<Grid container justifyContent="center">
<Tooltip title="Create Personal Account">
<IconButton
color="primary"
onClick={() => setShowPersonalAccountCreationModal(true)}
>
<Add />
</IconButton>
</Tooltip>
</Grid>
<CreateAccountModal
show={showPersonalAccountCreationModal}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowPersonalAccountCreationModal(false);
}
}}
group={group}
/>
<EditAccountModal
show={showPersonalAccountEditModal}
onClose={closeAccountEdit}
account={accountToEdit}
group={group}
/>
</>
)}
</TabPanel>
<TabPanel value="clearing">
<List>
{clearingAccounts.length === 0 ? (
<Alert severity="info">No Accounts</Alert>
) : (
<>
<ListItem>
<Input
value={searchValueClearing}
onChange={(e) => setSearchValueClearing(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValueClearing("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
</ListItem>
<Divider />
{filteredClearingAccounts.map((account) => (
<ListItem sx={{ padding: 0 }} key={account.id}>
<ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
<ListItemText primary={account.name} secondary={account.description} />
</ListItemLink>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() => openClearingAccountEdit(account)}
>
<Edit />
</IconButton>
<IconButton
color="primary"
onClick={() => copyClearingAccount(account)}
>
<ContentCopy />
</IconButton>
<IconButton
color="error"
onClick={() => setAccountToDelete(account.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</>
)}
</List>
{userPermissions.can_write && (
<>
<Grid container justifyContent="center">
<Tooltip title="Create Clearing Account">
<IconButton color="primary" onClick={openCreateDialog}>
<Add />
</IconButton>
</Tooltip>
</Grid>
<CreateClearingAccountModal
show={showClearingAccountCreationModal}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowClearingAccountCreationModal(false);
}
}}
initialValues={clearingAccountToCopy}
group={group}
/>
<EditClearingAccountModal
show={showClearingAccountEditModal}
onClose={closeClearingAccountEdit}
account={clearingAccountToEdit}
group={group}
/>
</>
)}
</TabPanel>
</TabContext>
</MobilePaper>
{userPermissions.can_write && (
<>
<SpeedDial
ariaLabel="Create Account"
sx={{ position: "fixed", bottom: 20, right: 20 }}
icon={<SpeedDialIcon />}
// onClose={() => setSpeedDialOpen(false)}
// onOpen={() => setSpeedDialOpen(true)}
onClick={toggleSpeedDial}
open={speedDialOpen}
>
<SpeedDialAction
icon={<PersonalAccountIcon />}
tooltipTitle="Personal"
tooltipOpen
onClick={() => setShowPersonalAccountCreationModal(true)}
/>
<SpeedDialAction
icon={<ClearingAccountIcon />}
tooltipTitle="Clearing"
tooltipOpen
onClick={openCreateDialog}
/>
</SpeedDial>
<Dialog maxWidth="xs" aria-labelledby="confirmation-dialog-title" open={accountToDelete !== null}>
<DialogTitle id="confirmation-dialog-title">Confirm delete account</DialogTitle>
<DialogContent dividers>
Are you sure you want to delete the account "
{allAccounts.find((acc) => acc.id === accountToDelete)?.name}"
</DialogContent>
<DialogActions>
<Button autoFocus onClick={() => setAccountToDelete(null)} color="primary">
Cancel
</Button>
<Button onClick={confirmDeleteAccount} color="error">
Ok
</Button>
</DialogActions>
</Dialog>
</>
)}
</>
);
}
Example #27
Source File: PurchaseDebitorShares.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function PurchaseDebitorShares({ group, transaction, showPositions = false }) {
const classes = useStyles();
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down("sm"));
const accounts = useRecoilValue(accountsSeenByUser(group.id));
const [searchValue, setSearchValue] = useState("");
const [filteredAccounts, setFilteredAccounts] = useState([]);
const [showAdvanced, setShowAdvanced] = useState(false);
const transactionHasPositions =
transaction.positions != null && transaction.positions.find((item) => !item.deleted) !== undefined;
const setLocalTransactionDetails = useSetRecoilState(pendingTransactionDetailChanges(transaction.id));
useEffect(() => {
for (const share of Object.values(transaction.debitor_shares)) {
if (share !== 1) {
setShowAdvanced(true);
break;
}
}
}, [transaction]);
useEffect(() => {
if (searchValue != null && searchValue !== "") {
setFilteredAccounts(
accounts.filter((acc) => {
return acc.name.toLowerCase().includes(searchValue.toLowerCase());
})
);
} else {
setFilteredAccounts(accounts);
}
}, [searchValue, accounts]);
const debitorShareValueForAccount = (accountID) => {
return transaction.debitor_shares && transaction.debitor_shares.hasOwnProperty(accountID)
? transaction.debitor_shares[accountID]
: 0;
};
const debitorValueForAccount = (accountID) => {
if (!transaction.account_balances.hasOwnProperty(accountID)) {
return 0.0;
}
return transaction.account_balances[accountID].common_debitors;
};
const positionValueForAccount = (accountID) => {
if (!transaction.account_balances.hasOwnProperty(accountID)) {
return 0.0;
}
return transaction.account_balances[accountID].positions;
};
const updateDebShare = (accountID, value) => {
if (value === 0) {
setLocalTransactionDetails((currState) => {
let newDebitorShares;
if (currState.debitor_shares === undefined) {
newDebitorShares = {
...transaction.debitor_shares,
};
} else {
newDebitorShares = {
...currState.debitor_shares,
};
}
delete newDebitorShares[accountID];
return {
...currState,
debitor_shares: newDebitorShares,
};
});
} else {
setLocalTransactionDetails((currState) => {
let newDebitorShares;
if (currState.debitor_shares === undefined) {
newDebitorShares = {
...transaction.debitor_shares,
[accountID]: value,
};
} else {
newDebitorShares = {
...currState.debitor_shares,
[accountID]: value,
};
}
return {
...currState,
debitor_shares: newDebitorShares,
};
});
}
};
return (
<div>
<Box className={classes.listItem}>
<Grid container direction="row" justifyContent="space-between">
<Typography variant="subtitle1" className={classes.checkboxLabel}>
<Box sx={{ display: "flex", alignItems: "flex-end" }}>For whom</Box>
</Typography>
{transaction.is_wip && (
<FormControlLabel
control={<Checkbox name={`show-advanced`} />}
checked={showAdvanced}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setShowAdvanced(event.target.checked)
}
label="Advanced"
/>
)}
</Grid>
</Box>
<Divider variant="middle" className={classes.divider} />
<TableContainer sx={{ maxHeight: { md: 400 } }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>
{isSmallScreen ? (
"Account"
) : (
<TextField
placeholder="Search ..."
margin="none"
size="small"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
variant="standard"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValue("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
)}
</TableCell>
<TableCell width="100px">Shares</TableCell>
{showPositions || transactionHasPositions ? (
<>
<TableCell width="100px" align="right">
Positions
</TableCell>
<TableCell width="3px" align="center">
+
</TableCell>
<TableCell width="100px" align="right">
Shared + Rest
</TableCell>
<TableCell width="3px" align="center">
=
</TableCell>
<TableCell width="100px" align="right">
Total
</TableCell>
</>
) : (
<TableCell width="100px" align="right">
Shared
</TableCell>
)}
</TableRow>
</TableHead>
<TableBody>
{filteredAccounts.map((account) => (
<AccountTableRow
key={account.id}
transaction={transaction}
account={account}
debitorValueForAccount={debitorValueForAccount}
debitorShareValueForAccount={debitorShareValueForAccount}
positionValueForAccount={positionValueForAccount}
showAdvanced={showAdvanced}
showPositions={showPositions}
updateDebShare={updateDebShare}
/>
))}
</TableBody>
</Table>
</TableContainer>
</div>
);
}
Example #28
Source File: TransactionList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function TransactionList({ group }) {
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const toggleSpeedDial = () => setSpeedDialOpen((currValue) => !currValue);
const [showTransferCreateDialog, setShowTransferCreateDialog] = useState(false);
const [showPurchaseCreateDialog, setShowPurchaseCreateDialog] = useState(false);
const transactions = useRecoilValue(transactionsSeenByUser(group.id));
const currentUser = useRecoilValue(userData);
const userPermissions = useRecoilValue(currUserPermissions(group.id));
const userAccounts = useRecoilValue(accountsOwnedByUser({ groupID: group.id, userID: currentUser.id }));
const groupAccountMap = useRecoilValue(accountIDsToName(group.id));
const theme: Theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("md"));
const [filteredTransactions, setFilteredTransactions] = useState([]);
const [searchValue, setSearchValue] = useState("");
const [sortMode, setSortMode] = useState("last_changed"); // last_changed, description, value, billed_at
useEffect(() => {
let filtered = transactions;
if (searchValue != null && searchValue !== "") {
filtered = transactions.filter((t) => t.filter(searchValue, groupAccountMap));
}
filtered = [...filtered].sort(getTransactionSortFunc(sortMode));
setFilteredTransactions(filtered);
}, [searchValue, setFilteredTransactions, sortMode, transactions, userAccounts]);
useTitle(`${group.name} - Transactions`);
const openPurchaseCreateDialog = () => {
setShowPurchaseCreateDialog(true);
};
const openTransferCreateDialog = () => {
setShowTransferCreateDialog(true);
};
return (
<>
<MobilePaper>
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", sm: "column", md: "row", lg: "row" },
alignItems: { md: "flex-end" },
pl: "16px",
justifyContent: "space-between",
}}
>
<Box sx={{ display: "flex-item" }}>
<Box sx={{ minWidth: "56px", pt: "16px" }}>
<SearchIcon sx={{ color: "action.active" }} />
</Box>
<Input
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
sx={{ pt: "16px" }}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValue("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
<FormControl variant="standard" sx={{ minWidth: 120, ml: 3 }}>
<InputLabel id="select-sort-by-label">Sort by</InputLabel>
<Select
labelId="select-sort-by-label"
id="select-sort-by"
label="Sort by"
onChange={(evt) => setSortMode(evt.target.value)}
value={sortMode}
>
<MenuItem value="last_changed">Last changed</MenuItem>
<MenuItem value="description">Description</MenuItem>
<MenuItem value="value">Value</MenuItem>
<MenuItem value="billed_at">Date</MenuItem>
</Select>
</FormControl>
</Box>
{!isSmallScreen && (
<Box sx={{ display: "flex-item" }}>
<div style={{ padding: "8px" }}>
<Add color="primary" />
</div>
<Tooltip title="Create Purchase">
<IconButton color="primary" onClick={openPurchaseCreateDialog}>
<PurchaseIcon />
</IconButton>
</Tooltip>
<Tooltip title="Create Transfer">
<IconButton color="primary" onClick={openTransferCreateDialog}>
<TransferIcon />
</IconButton>
</Tooltip>
</Box>
)}
</Box>
<Divider sx={{ mt: 1 }} />
<List>
{transactions.length === 0 ? (
<Alert severity="info">No Transactions</Alert>
) : (
filteredTransactions.map((transaction) => (
<TransactionListEntry key={transaction.id} group={group} transaction={transaction} />
))
)}
</List>
<TransferCreateModal
group={group}
show={showTransferCreateDialog}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowTransferCreateDialog(false);
}
}}
/>
<PurchaseCreateModal
group={group}
show={showPurchaseCreateDialog}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowPurchaseCreateDialog(false);
}
}}
/>
</MobilePaper>
{userPermissions.can_write && (
<SpeedDial
ariaLabel="Create Account"
sx={{ position: "fixed", bottom: 20, right: 20 }}
icon={<SpeedDialIcon />}
// onClose={() => setSpeedDialOpen(false)}
// onOpen={() => setSpeedDialOpen(true)}
onClick={toggleSpeedDial}
open={speedDialOpen}
>
<SpeedDialAction
icon={<PurchaseIcon />}
tooltipTitle="Purchase"
tooltipOpen
onClick={openPurchaseCreateDialog}
/>
<SpeedDialAction
icon={<TransferIcon />}
tooltipTitle="Transfer"
tooltipOpen
onClick={openTransferCreateDialog}
/>
</SpeedDial>
)}
</>
);
}
Example #29
Source File: ClearingSharesFormElement.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function ClearingSharesFormElement({ group, clearingShares, setClearingShares, accountID = undefined }) {
const accounts = useRecoilValue(accountsSeenByUser(group.id));
const [showAdvanced, setShowAdvanced] = useState(false);
const [searchValue, setSearchValue] = useState("");
const [filteredAccounts, setFilteredAccounts] = useState([]);
useEffect(() => {
if (searchValue != null && searchValue !== "") {
setFilteredAccounts(
accounts.filter((acc) => {
return acc.name.toLowerCase().includes(searchValue.toLowerCase());
})
);
} else {
setFilteredAccounts(accounts);
}
}, [searchValue, accounts]);
return (
<>
<Grid container direction="row" justifyContent="space-between">
<Typography variant="subtitle1">Allocation to</Typography>
<FormControlLabel
control={<Checkbox name={`show-advanced`} />}
checked={showAdvanced}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setShowAdvanced(event.target.checked)}
label="Advanced"
/>
</Grid>
<TableContainer sx={{ maxHeight: 400 }}>
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>
<TextField
placeholder="Search ..."
margin="none"
size="small"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
variant="standard"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</TableCell>
<TableCell width="100px">Shares</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredAccounts.map(
(account) =>
(accountID === undefined || account.id !== accountID) && (
<TableRow hover key={account.id}>
<TableCell>
<Grid container direction="row" alignItems="center">
<Grid item>
{account.type === "personal" ? <Person /> : <CompareArrows />}
</Grid>
<Grid item sx={{ ml: 1 }}>
<Typography variant="body2" component="span">
{account.name}
</Typography>
</Grid>
</Grid>
</TableCell>
<TableCell width="100px">
{showAdvanced ? (
<ShareInput
onChange={(value) =>
setClearingShares({
...(clearingShares !== undefined ? clearingShares : {}),
[account.id]: value,
})
}
value={
clearingShares && clearingShares.hasOwnProperty(account.id)
? clearingShares[account.id]
: 0.0
}
/>
) : (
<Checkbox
name={`${account.name}-checked`}
checked={
clearingShares &&
clearingShares.hasOwnProperty(account.id) &&
clearingShares[account.id] !== 0
}
onChange={(event) =>
setClearingShares({
...(clearingShares !== undefined ? clearingShares : {}),
[account.id]: event.target.checked ? 1.0 : 0.0,
})
}
/>
)}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</TableContainer>
</>
);
}