Reserved topic stuff
parent
6c0429351a
commit
a91da7cf2c
|
@ -108,6 +108,7 @@ type Config struct {
|
|||
EnableLogin bool
|
||||
EnableEmailConfirm bool
|
||||
EnableResetPassword bool
|
||||
EnableAccountUpgrades bool
|
||||
Version string // injected by App
|
||||
}
|
||||
|
||||
|
|
|
@ -40,12 +40,12 @@ import (
|
|||
message cache duration
|
||||
Keep 10000 messages or keep X days?
|
||||
Attachment expiration based on plan
|
||||
reserve topics
|
||||
purge accounts that were not logged into in X
|
||||
reset daily limits for users
|
||||
Account usage not updated "in real time"
|
||||
max token issue limit
|
||||
user db startup queries -> foreign keys
|
||||
UI
|
||||
- Feature flag for "reserve topic" feature
|
||||
Sync:
|
||||
- "mute" setting
|
||||
- figure out what settings are "web" or "phone"
|
||||
|
@ -447,17 +447,20 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
|
|||
if !s.config.WebRootIsApp {
|
||||
appRoot = "/app"
|
||||
}
|
||||
disallowedTopicsStr := `"` + strings.Join(disallowedTopics, `", "`) + `"`
|
||||
response := &apiConfigResponse{
|
||||
BaseURL: "", // Will translate to window.location.origin
|
||||
AppRoot: appRoot,
|
||||
EnableLogin: s.config.EnableLogin,
|
||||
EnableSignup: s.config.EnableSignup,
|
||||
EnableResetPassword: s.config.EnableResetPassword,
|
||||
DisallowedTopics: disallowedTopics,
|
||||
}
|
||||
b, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
_, err := io.WriteString(w, fmt.Sprintf(`// Generated server configuration
|
||||
var config = {
|
||||
baseUrl: window.location.origin,
|
||||
appRoot: "%s",
|
||||
enableLogin: %t,
|
||||
enableSignup: %t,
|
||||
enableResetPassword: %t,
|
||||
disallowedTopics: [%s],
|
||||
};`, appRoot, s.config.EnableLogin, s.config.EnableSignup, s.config.EnableResetPassword, disallowedTopicsStr))
|
||||
_, err = io.WriteString(w, fmt.Sprintf("// Generated server configuration\nvar config = %s;\n", string(b)))
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -280,3 +280,12 @@ type apiAccountAccessRequest struct {
|
|||
Topic string `json:"topic"`
|
||||
Everyone string `json:"everyone"`
|
||||
}
|
||||
|
||||
type apiConfigResponse struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
AppRoot string `json:"app_root"`
|
||||
EnableLogin bool `json:"enable_login"`
|
||||
EnableSignup bool `json:"enable_signup"`
|
||||
EnableResetPassword bool `json:"enable_reset_password"`
|
||||
DisallowedTopics []string `json:"disallowed_topics"`
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
"subscription_settings_dialog_title": "Subscription settings",
|
||||
"subscription_settings_dialog_description": "Configure settings specifically for this topic subscription. Settings are currently only applied locally.",
|
||||
"subscription_settings_dialog_display_name_placeholder": "Display name",
|
||||
"subscription_settings_dialog_reserve_topic_label": "Reserve topic and configure access",
|
||||
"subscription_settings_button_cancel": "Cancel",
|
||||
"subscription_settings_button_save": "Save",
|
||||
"notifications_loading": "Loading notifications …",
|
||||
|
@ -159,6 +160,7 @@
|
|||
"subscribe_dialog_login_button_back": "Back",
|
||||
"subscribe_dialog_login_button_login": "Login",
|
||||
"subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized",
|
||||
"subscribe_dialog_error_topic_already_reserved": "Topic already reserved",
|
||||
"subscribe_dialog_error_user_anonymous": "anonymous",
|
||||
"account_basics_title": "Account",
|
||||
"account_basics_username_title": "Username",
|
||||
|
@ -253,6 +255,7 @@
|
|||
"prefs_reservations_table_everyone_read_write": "Everyone can publish and subscribe",
|
||||
"prefs_reservations_dialog_title_add": "Reserve topic",
|
||||
"prefs_reservations_dialog_title_edit": "Edit reserved topic",
|
||||
"prefs_reservations_dialog_description": "Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.",
|
||||
"prefs_reservations_dialog_topic_label": "Topic",
|
||||
"prefs_reservations_dialog_access_label": "Access",
|
||||
"priority_min": "min",
|
||||
|
|
|
@ -231,6 +231,8 @@ class AccountApi {
|
|||
});
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new UnauthorizedError();
|
||||
} else if (response.status === 409) {
|
||||
throw new TopicReservedError();
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
|
@ -312,6 +314,13 @@ export class UsernameTakenError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class TopicReservedError extends Error {
|
||||
constructor(topic) {
|
||||
super("Topic already reserved");
|
||||
this.topic = topic;
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountCreateLimitReachedError extends Error {
|
||||
constructor() {
|
||||
super("Account creation limit reached");
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
const config = window.config;
|
||||
|
||||
if (config.base_url === "") {
|
||||
config.base_url = window.location.origin;
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
|
|
@ -177,7 +177,7 @@ const Stats = () => {
|
|||
<PrefGroup>
|
||||
<Pref title={t("account_usage_plan_title")}>
|
||||
<div>
|
||||
{account?.role === "admin"
|
||||
{account.role === "admin"
|
||||
? <>{t("account_usage_unlimited")} <Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
|
||||
: t(`account_usage_plan_code_${planCode}`)}
|
||||
</div>
|
||||
|
@ -187,28 +187,44 @@ const Stats = () => {
|
|||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.topics}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.topics > 0 ? t("account_usage_of_limit", { limit: account.limits.topics }) : t("account_usage_unlimited")}</Typography>
|
||||
</div>
|
||||
<LinearProgress variant="determinate" value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100} />
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={account.limits.topics > 0 ? normalize(account.stats.topics, account.limits.topics) : 100}
|
||||
color={account?.role !== "admin" && account.stats.topics_remaining === 0 ? 'error' : 'primary'}
|
||||
/>
|
||||
</Pref>
|
||||
<Pref title={t("account_usage_messages_title")}>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.messages > 0 ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
|
||||
</div>
|
||||
<LinearProgress variant="determinate" value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100} />
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={account.limits.messages > 0 ? normalize(account.stats.messages, account.limits.messages) : 100}
|
||||
color={account?.role !== "admin" && account.stats.messages_remaining === 0 ? 'error' : 'primary'}
|
||||
/>
|
||||
</Pref>
|
||||
<Pref title={t("account_usage_emails_title")}>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.emails > 0 ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
|
||||
</div>
|
||||
<LinearProgress variant="determinate" value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={account.limits.emails > 0 ? normalize(account.stats.emails, account.limits.emails) : 100}
|
||||
color={account?.role !== "admin" && account.stats.emails_remaining === 0 ? 'error' : 'primary'}
|
||||
/>
|
||||
</Pref>
|
||||
<Pref title={t("account_usage_attachment_storage_title")} subtitle={t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) })}>
|
||||
<Pref title={t("account_usage_attachment_storage_title")} subtitle={account.role !== "admin" ? t("account_usage_attachment_storage_subtitle", { filesize: formatBytes(account.limits.attachment_file_size) }) : null}>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
|
||||
<Typography variant="body2" sx={{float: "right"}}>{account.limits.attachment_total_size > 0 ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
|
||||
</div>
|
||||
<LinearProgress variant="determinate" value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={account.limits.attachment_total_size > 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
|
||||
color={account.role !== "admin" && account.stats.attachment_total_size_remaining === 0 ? 'error' : 'primary'}
|
||||
/>
|
||||
</Pref>
|
||||
</PrefGroup>
|
||||
{account.limits.basis === "ip" &&
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {
|
||||
Alert,
|
||||
CardActions,
|
||||
CardContent,
|
||||
FormControl,
|
||||
|
@ -44,6 +45,8 @@ import LockIcon from "@mui/icons-material/Lock";
|
|||
import {Public, PublicOff} from "@mui/icons-material";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||
|
||||
const Preferences = () => {
|
||||
return (
|
||||
|
@ -482,10 +485,11 @@ const Reservations = () => {
|
|||
const [dialogKey, setDialogKey] = useState(0);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
if (!session.exists() || !account) {
|
||||
if (!session.exists() || !account || account.role === "admin") {
|
||||
return <></>;
|
||||
}
|
||||
const reservations = account.reservations || [];
|
||||
const limitReached = account.role === "user" && account.stats.topics_remaining === 0;
|
||||
|
||||
const handleAddClick = () => {
|
||||
setDialogKey(prev => prev+1);
|
||||
|
@ -505,7 +509,7 @@ const Reservations = () => {
|
|||
} catch (e) {
|
||||
console.log(`[Preferences] Error topic reservation.`, e);
|
||||
}
|
||||
// FIXME handle 401/403
|
||||
// FIXME handle 401/403/409
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -518,9 +522,15 @@ const Reservations = () => {
|
|||
{t("prefs_reservations_description")}
|
||||
</Paragraph>
|
||||
{reservations.length > 0 && <ReservationsTable reservations={reservations}/>}
|
||||
{limitReached &&
|
||||
<Alert severity="info">
|
||||
You reached your reserved topics limit.
|
||||
</Alert>
|
||||
}
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button onClick={handleAddClick}>{t("prefs_reservations_add_button")}</Button>
|
||||
<Button onClick={handleAddClick} disabled={limitReached}>{t("prefs_reservations_add_button")}</Button>
|
||||
|
||||
<ReservationsDialog
|
||||
key={`reservationAddDialog${dialogKey}`}
|
||||
open={dialogOpen}
|
||||
|
@ -559,7 +569,7 @@ const ReservationsTable = (props) => {
|
|||
} catch (e) {
|
||||
console.log(`[Preferences] Error topic reservation.`, e);
|
||||
}
|
||||
// FIXME handle 401/403
|
||||
// FIXME handle 401/403/409
|
||||
};
|
||||
|
||||
const handleDeleteClick = async (reservation) => {
|
||||
|
@ -670,6 +680,9 @@ const ReservationsDialog = (props) => {
|
|||
<Dialog open={props.open} onClose={props.onCancel} maxWidth="sm" fullWidth fullScreen={fullScreen}>
|
||||
<DialogTitle>{editMode ? t("prefs_reservations_dialog_title_edit") : t("prefs_reservations_dialog_title_add")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("prefs_reservations_dialog_description")}
|
||||
</DialogContentText>
|
||||
{!editMode && <TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
|
@ -682,37 +695,11 @@ const ReservationsDialog = (props) => {
|
|||
fullWidth
|
||||
variant="standard"
|
||||
/>}
|
||||
<FormControl fullWidth variant="standard">
|
||||
<Select
|
||||
<ReserveTopicSelect
|
||||
value={everyone}
|
||||
onChange={(ev) => setEveryone(ev.target.value)}
|
||||
aria-label={t("prefs_reservations_dialog_access_label")}
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
"& .MuiSelect-select": {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="deny-all">
|
||||
<ListItemIcon><LockIcon /></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")} />
|
||||
</MenuItem>
|
||||
<MenuItem value="read-only">
|
||||
<ListItemIcon><PublicOff /></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")} />
|
||||
</MenuItem>
|
||||
<MenuItem value="write-only">
|
||||
<ListItemIcon><PublicOff /></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")} />
|
||||
</MenuItem>
|
||||
<MenuItem value="read-write">
|
||||
<ListItemIcon><Public /></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
onChange={setEveryone}
|
||||
sx={{mt: 1}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button>
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import * as React from 'react';
|
||||
import {useState} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Checkbox, FormControl, FormControlLabel, Select, useMediaQuery} from "@mui/material";
|
||||
import theme from "./theme";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import DialogFooter from "./DialogFooter";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||
import session from "../app/Session";
|
||||
import routes from "./routes";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import {Public, PublicOff} from "@mui/icons-material";
|
||||
|
||||
const ReserveTopicSelect = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const sx = props.sx || {};
|
||||
return (
|
||||
<FormControl fullWidth variant="standard" sx={sx}>
|
||||
<Select
|
||||
value={props.value}
|
||||
onChange={(ev) => props.onChange(ev.target.value)}
|
||||
aria-label={t("prefs_reservations_dialog_access_label")}
|
||||
sx={{
|
||||
"& .MuiSelect-select": {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="deny-all">
|
||||
<ListItemIcon><LockIcon/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/>
|
||||
</MenuItem>
|
||||
<MenuItem value="read-only">
|
||||
<ListItemIcon><PublicOff/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/>
|
||||
</MenuItem>
|
||||
<MenuItem value="write-only">
|
||||
<ListItemIcon><PublicOff/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/>
|
||||
</MenuItem>
|
||||
<MenuItem value="read-write">
|
||||
<ListItemIcon><Public/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReserveTopicSelect;
|
|
@ -6,7 +6,7 @@ import Dialog from '@mui/material/Dialog';
|
|||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
||||
import {Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery} from "@mui/material";
|
||||
import theme from "./theme";
|
||||
import api from "../app/Api";
|
||||
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
|
||||
|
@ -17,14 +17,14 @@ import DialogFooter from "./DialogFooter";
|
|||
import {useTranslation} from "react-i18next";
|
||||
import session from "../app/Session";
|
||||
import routes from "./routes";
|
||||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi";
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import PublicOffIcon from '@mui/icons-material/PublicOff';
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import PopupMenu from "./PopupMenu";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||
|
||||
const publicBaseUrl = "https://ntfy.sh";
|
||||
|
||||
|
@ -33,6 +33,7 @@ const SubscribeDialog = (props) => {
|
|||
const [topic, setTopic] = useState("");
|
||||
const [showLoginPage, setShowLoginPage] = useState(false);
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const handleSuccess = async () => {
|
||||
console.log(`[SubscribeDialog] Subscribing to topic ${topic}`);
|
||||
const actualBaseUrl = (baseUrl) ? baseUrl : config.baseUrl;
|
||||
|
@ -44,6 +45,7 @@ const SubscribeDialog = (props) => {
|
|||
topic: topic
|
||||
});
|
||||
await subscriptionManager.setRemoteId(subscription.id, remoteSubscription.id);
|
||||
await accountApi.sync();
|
||||
} catch (e) {
|
||||
console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
|
@ -54,6 +56,7 @@ const SubscribeDialog = (props) => {
|
|||
poller.pollInBackground(subscription); // Dangle!
|
||||
props.onSuccess(subscription);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||
{!showLoginPage && <SubscribePage
|
||||
|
@ -78,10 +81,11 @@ const SubscribeDialog = (props) => {
|
|||
|
||||
const SubscribePage = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [reserveTopicVisible, setReserveTopicVisible] = useState(false);
|
||||
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [accessAnchorEl, setAccessAnchorEl] = useState(null);
|
||||
const [access, setAccess] = useState("public");
|
||||
const [everyone, setEveryone] = useState("deny-all");
|
||||
const baseUrl = (anotherServerVisible) ? props.baseUrl : config.baseUrl;
|
||||
const topic = props.topic;
|
||||
const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
|
||||
|
@ -92,6 +96,8 @@ const SubscribePage = (props) => {
|
|||
const handleSubscribe = async () => {
|
||||
const user = await userManager.get(baseUrl); // May be undefined
|
||||
const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
|
||||
|
||||
// Check read access to topic
|
||||
const success = await api.topicAuth(baseUrl, topic, user);
|
||||
if (!success) {
|
||||
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
|
||||
|
@ -103,6 +109,24 @@ const SubscribePage = (props) => {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve topic (if requested)
|
||||
if (session.exists() && baseUrl === config.baseUrl && reserveTopicVisible) {
|
||||
console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`);
|
||||
try {
|
||||
await accountApi.upsertAccess(topic, everyone);
|
||||
// Account sync later after it was added
|
||||
} catch (e) {
|
||||
console.log(`[SubscribeDialog] Error reserving topic`, e);
|
||||
if ((e instanceof UnauthorizedError)) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
} else if ((e instanceof TopicReservedError)) {
|
||||
setErrorText(t("subscribe_dialog_error_topic_already_reserved"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
|
||||
props.onSuccess();
|
||||
};
|
||||
|
@ -137,14 +161,7 @@ const SubscribePage = (props) => {
|
|||
<DialogContentText>
|
||||
{t("subscribe_dialog_subscribe_description")}
|
||||
</DialogContentText>
|
||||
<div style={{display: 'flex'}} role="row">
|
||||
{session.exists() &&
|
||||
<IconButton onClick={(ev) => setAccessAnchorEl(ev.currentTarget)} color="inherit" size="large" edge="start" sx={{height: "45px", marginTop: "5px", color: "grey"}}>
|
||||
{access === "public" && <PublicIcon/>}
|
||||
{access === "public-read" && <PublicOffIcon/>}
|
||||
{access === "private" && <LockIcon/>}
|
||||
</IconButton>
|
||||
}
|
||||
<div style={{display: 'flex', paddingBottom: "8px"}} role="row">
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
|
@ -168,19 +185,19 @@ const SubscribePage = (props) => {
|
|||
open={!!accessAnchorEl}
|
||||
onClose={() => setAccessAnchorEl(null)}
|
||||
>
|
||||
<MenuItem onClick={() => setAccess("private")} selected={access === "private"}>
|
||||
<MenuItem onClick={() => setEveryone("private")} selected={everyone === "private"}>
|
||||
<ListItemIcon>
|
||||
<LockIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
Only I can publish and subscribe
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setAccess("public-read")} selected={access === "public-read"}>
|
||||
<MenuItem onClick={() => setEveryone("public-read")} selected={everyone === "public-read"}>
|
||||
<ListItemIcon>
|
||||
<PublicOffIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
I can publish, everyone can subscribe
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => setAccess("public")} selected={access === "public"}>
|
||||
<MenuItem onClick={() => setEveryone("public")} selected={everyone === "public"}>
|
||||
<ListItemIcon>
|
||||
<PublicIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
|
@ -188,8 +205,33 @@ const SubscribePage = (props) => {
|
|||
</MenuItem>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
{session.exists() && !anotherServerVisible &&
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
variant="standard"
|
||||
control={
|
||||
<Checkbox
|
||||
fullWidth
|
||||
checked={reserveTopicVisible}
|
||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||
inputProps={{
|
||||
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={t("subscription_settings_dialog_reserve_topic_label")}
|
||||
/>
|
||||
{reserveTopicVisible &&
|
||||
<ReserveTopicSelect
|
||||
value={everyone}
|
||||
onChange={setEveryone}
|
||||
/>
|
||||
}
|
||||
</FormGroup>
|
||||
}
|
||||
{!reserveTopicVisible &&
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
sx={{pt: 1}}
|
||||
control={
|
||||
<Checkbox
|
||||
onChange={handleUseAnotherChanged}
|
||||
|
@ -198,14 +240,13 @@ const SubscribePage = (props) => {
|
|||
}}
|
||||
/>
|
||||
}
|
||||
label={t("subscribe_dialog_subscribe_use_another_label")} />
|
||||
label={t("subscribe_dialog_subscribe_use_another_label")}/>
|
||||
{anotherServerVisible && <Autocomplete
|
||||
freeSolo
|
||||
options={existingBaseUrls}
|
||||
sx={{ maxWidth: 400 }}
|
||||
inputValue={props.baseUrl}
|
||||
onInputChange={updateBaseUrl}
|
||||
renderInput={ (params) =>
|
||||
renderInput={(params) =>
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder={config.baseUrl}
|
||||
|
@ -214,6 +255,8 @@ const SubscribePage = (props) => {
|
|||
/>
|
||||
}
|
||||
/>}
|
||||
</FormGroup>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogFooter status={errorText}>
|
||||
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
|
||||
|
|
|
@ -6,7 +6,7 @@ import Dialog from '@mui/material/Dialog';
|
|||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Checkbox, FormControl, FormControlLabel, Select, useMediaQuery} from "@mui/material";
|
||||
import {Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
||||
import theme from "./theme";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import DialogFooter from "./DialogFooter";
|
||||
|
@ -14,11 +14,7 @@ import {useTranslation} from "react-i18next";
|
|||
import accountApi, {UnauthorizedError} from "../app/AccountApi";
|
||||
import session from "../app/Session";
|
||||
import routes from "./routes";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import {Public, PublicOff} from "@mui/icons-material";
|
||||
import ReserveTopicSelect from "./ReserveTopicSelect";
|
||||
|
||||
const SubscriptionSettingsDialog = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -53,6 +49,8 @@ const SubscriptionSettingsDialog = (props) => {
|
|||
if ((e instanceof UnauthorizedError)) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
}
|
||||
|
||||
// FIXME handle 409
|
||||
}
|
||||
}
|
||||
props.onClose();
|
||||
|
@ -80,7 +78,6 @@ const SubscriptionSettingsDialog = (props) => {
|
|||
"aria-label": t("subscription_settings_dialog_display_name_placeholder")
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
fullWidth
|
||||
variant="standard"
|
||||
|
@ -90,45 +87,17 @@ const SubscriptionSettingsDialog = (props) => {
|
|||
checked={reserveTopicVisible}
|
||||
onChange={(ev) => setReserveTopicVisible(ev.target.checked)}
|
||||
inputProps={{
|
||||
"aria-label": t("xxxxxxxxxxxxxxxxxx")
|
||||
"aria-label": t("subscription_settings_dialog_reserve_topic_label")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={t("Reserve topic and configure custom access:")}
|
||||
label={t("subscription_settings_dialog_reserve_topic_label")}
|
||||
/>
|
||||
{reserveTopicVisible &&
|
||||
<FormControl variant="standard">
|
||||
<Select
|
||||
<ReserveTopicSelect
|
||||
value={everyone}
|
||||
onChange={(ev) => setEveryone(ev.target.value)}
|
||||
aria-label={t("prefs_reservations_dialog_access_label")}
|
||||
sx={{
|
||||
"& .MuiSelect-select": {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="deny-all">
|
||||
<ListItemIcon><LockIcon/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/>
|
||||
</MenuItem>
|
||||
<MenuItem value="read-only">
|
||||
<ListItemIcon><PublicOff/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/>
|
||||
</MenuItem>
|
||||
<MenuItem value="write-only">
|
||||
<ListItemIcon><PublicOff/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/>
|
||||
</MenuItem>
|
||||
<MenuItem value="read-write">
|
||||
<ListItemIcon><Public/></ListItemIcon>
|
||||
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
onChange={setEveryone}
|
||||
/>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
|
|
Loading…
Reference in New Issue