Update Share Object UI to reflect max expiration time (#3098)
This commit is contained in:
@@ -251,6 +251,36 @@ export const erasureCodeCalc = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 92400 seconds -> 1 day, 1 hour, 40 minutes.
|
||||||
|
export const niceTimeFromSeconds = (seconds: number): string => {
|
||||||
|
const days = Math.floor(seconds / (3600 * 24));
|
||||||
|
const hours = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
parts.push(`${days} day${days !== 1 ? "s" : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
parts.push(`${hours} hour${hours !== 1 ? "s" : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingSeconds > 0) {
|
||||||
|
parts.push(
|
||||||
|
`${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(" and ");
|
||||||
|
};
|
||||||
|
|
||||||
// seconds / minutes /hours / Days / Years calculator
|
// seconds / minutes /hours / Days / Years calculator
|
||||||
export const niceDays = (secondsValue: string, timeVariant: string = "s") => {
|
export const niceDays = (secondsValue: string, timeVariant: string = "s") => {
|
||||||
let seconds = parseFloat(secondsValue);
|
let seconds = parseFloat(secondsValue);
|
||||||
@@ -258,6 +288,7 @@ export const niceDays = (secondsValue: string, timeVariant: string = "s") => {
|
|||||||
return niceDaysInt(seconds, timeVariant);
|
return niceDaysInt(seconds, timeVariant);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// niceDaysInt returns the string in the max unit found e.g. 92400 seconds -> 1 day
|
||||||
export const niceDaysInt = (seconds: number, timeVariant: string = "s") => {
|
export const niceDaysInt = (seconds: number, timeVariant: string = "s") => {
|
||||||
switch (timeVariant) {
|
switch (timeVariant) {
|
||||||
case "ns":
|
case "ns":
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const initialState: BucketDetailsState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const bucketDetailsSlice = createSlice({
|
export const bucketDetailsSlice = createSlice({
|
||||||
name: "trace",
|
name: "bucketDetails",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setBucketDetailsTab: (state, action: PayloadAction<string>) => {
|
setBucketDetailsTab: (state, action: PayloadAction<string>) => {
|
||||||
|
|||||||
@@ -16,11 +16,22 @@
|
|||||||
|
|
||||||
import React, { Fragment, useEffect, useState } from "react";
|
import React, { Fragment, useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { Button, CopyIcon, ReadBox, ShareIcon, Grid, ProgressBar } from "mds";
|
import {
|
||||||
|
Button,
|
||||||
|
CopyIcon,
|
||||||
|
ReadBox,
|
||||||
|
ShareIcon,
|
||||||
|
Grid,
|
||||||
|
ProgressBar,
|
||||||
|
Tooltip,
|
||||||
|
} from "mds";
|
||||||
import CopyToClipboard from "react-copy-to-clipboard";
|
import CopyToClipboard from "react-copy-to-clipboard";
|
||||||
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
|
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
|
||||||
import DaysSelector from "../../../../Common/FormComponents/DaysSelector/DaysSelector";
|
import DaysSelector from "../../../../Common/FormComponents/DaysSelector/DaysSelector";
|
||||||
import { encodeURLString } from "../../../../../../common/utils";
|
import {
|
||||||
|
encodeURLString,
|
||||||
|
niceTimeFromSeconds,
|
||||||
|
} from "../../../../../../common/utils";
|
||||||
import {
|
import {
|
||||||
selDistSet,
|
selDistSet,
|
||||||
setModalErrorSnackMessage,
|
setModalErrorSnackMessage,
|
||||||
@@ -30,6 +41,8 @@ import { useAppDispatch } from "../../../../../../store";
|
|||||||
import { BucketObject } from "api/consoleApi";
|
import { BucketObject } from "api/consoleApi";
|
||||||
import { api } from "api";
|
import { api } from "api";
|
||||||
import { errorToHandler } from "api/errors";
|
import { errorToHandler } from "api/errors";
|
||||||
|
import { getMaxShareLinkExpTime } from "screens/Console/ObjectBrowser/objectBrowserThunks";
|
||||||
|
import { maxShareLinkExpTime } from "screens/Console/ObjectBrowser/objectBrowserSlice";
|
||||||
|
|
||||||
interface IShareFileProps {
|
interface IShareFileProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -46,6 +59,7 @@ const ShareFile = ({
|
|||||||
}: IShareFileProps) => {
|
}: IShareFileProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const distributedSetup = useSelector(selDistSet);
|
const distributedSetup = useSelector(selDistSet);
|
||||||
|
const maxshareLinkExpTimeVal = useSelector(maxShareLinkExpTime);
|
||||||
const [shareURL, setShareURL] = useState<string>("");
|
const [shareURL, setShareURL] = useState<string>("");
|
||||||
const [isLoadingVersion, setIsLoadingVersion] = useState<boolean>(true);
|
const [isLoadingVersion, setIsLoadingVersion] = useState<boolean>(true);
|
||||||
const [isLoadingFile, setIsLoadingFile] = useState<boolean>(false);
|
const [isLoadingFile, setIsLoadingFile] = useState<boolean>(false);
|
||||||
@@ -65,6 +79,10 @@ const ShareFile = ({
|
|||||||
setShareURL("");
|
setShareURL("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(getMaxShareLinkExpTime());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// In case version is undefined, we get the latest version of the object
|
// In case version is undefined, we get the latest version of the object
|
||||||
if (dataObject.version_id === undefined) {
|
if (dataObject.version_id === undefined) {
|
||||||
@@ -172,11 +190,28 @@ const ShareFile = ({
|
|||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
This is a temporary URL with integrated access credentials for
|
<Tooltip
|
||||||
sharing objects valid for up to 7 days.
|
placement="right"
|
||||||
<br />
|
tooltip={
|
||||||
<br />
|
<span>
|
||||||
The temporary URL expires after the configured time limit.
|
You can reset your session by logging out and logging back
|
||||||
|
in to the web UI. <br /> <br />
|
||||||
|
You can increase the maximum configuration time by setting
|
||||||
|
the MINIO_STS_DURATION environment variable on all your
|
||||||
|
nodes. <br /> <br />
|
||||||
|
You can use <b>mc share</b> as an alternative to this UI,
|
||||||
|
where the session length does not limit the URL validity.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
The following URL lets you share this object without requiring
|
||||||
|
a login. <br />
|
||||||
|
The URL expires automatically at the earlier of your
|
||||||
|
configured time ({niceTimeFromSeconds(maxshareLinkExpTimeVal)}
|
||||||
|
) or the expiration of your current web session.
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
</Grid>
|
</Grid>
|
||||||
<br />
|
<br />
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
@@ -184,7 +219,7 @@ const ShareFile = ({
|
|||||||
initialDate={initialDate}
|
initialDate={initialDate}
|
||||||
id="date"
|
id="date"
|
||||||
label="Active for"
|
label="Active for"
|
||||||
maxDays={7}
|
maxSeconds={maxshareLinkExpTimeVal}
|
||||||
onChange={dateChanged}
|
onChange={dateChanged}
|
||||||
entity="Link"
|
entity="Link"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,10 +18,14 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { Box, InputBox, InputLabel, LinkIcon } from "mds";
|
import { Box, InputBox, InputLabel, LinkIcon } from "mds";
|
||||||
|
|
||||||
|
const DAY_SECONDS = 86400;
|
||||||
|
const HOUR_SECONDS = 3600;
|
||||||
|
const HOUR_MINUTES = 60;
|
||||||
|
|
||||||
interface IDaysSelector {
|
interface IDaysSelector {
|
||||||
id: string;
|
id: string;
|
||||||
initialDate: Date;
|
initialDate: Date;
|
||||||
maxDays?: number;
|
maxSeconds: number;
|
||||||
label: string;
|
label: string;
|
||||||
entity: string;
|
entity: string;
|
||||||
onChange: (newDate: string, isValid: boolean) => void;
|
onChange: (newDate: string, isValid: boolean) => void;
|
||||||
@@ -43,16 +47,27 @@ const DaysSelector = ({
|
|||||||
id,
|
id,
|
||||||
initialDate,
|
initialDate,
|
||||||
label,
|
label,
|
||||||
maxDays,
|
maxSeconds,
|
||||||
entity,
|
entity,
|
||||||
onChange,
|
onChange,
|
||||||
}: IDaysSelector) => {
|
}: IDaysSelector) => {
|
||||||
const [selectedDays, setSelectedDays] = useState<number>(7);
|
const maxDays = Math.floor(maxSeconds / DAY_SECONDS);
|
||||||
|
const maxHours = Math.floor((maxSeconds % DAY_SECONDS) / HOUR_SECONDS);
|
||||||
|
const maxMinutes = Math.floor((maxSeconds % HOUR_SECONDS) / HOUR_MINUTES);
|
||||||
|
|
||||||
|
const [selectedDays, setSelectedDays] = useState<number>(0);
|
||||||
const [selectedHours, setSelectedHours] = useState<number>(0);
|
const [selectedHours, setSelectedHours] = useState<number>(0);
|
||||||
const [selectedMinutes, setSelectedMinutes] = useState<number>(0);
|
const [selectedMinutes, setSelectedMinutes] = useState<number>(0);
|
||||||
const [validDate, setValidDate] = useState<boolean>(true);
|
const [validDate, setValidDate] = useState<boolean>(true);
|
||||||
const [dateSelected, setDateSelected] = useState<DateTime>(DateTime.now());
|
const [dateSelected, setDateSelected] = useState<DateTime>(DateTime.now());
|
||||||
|
|
||||||
|
// Set initial values
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedDays(maxDays);
|
||||||
|
setSelectedHours(maxHours);
|
||||||
|
setSelectedMinutes(maxMinutes);
|
||||||
|
}, [maxDays, maxHours, maxMinutes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isNaN(selectedHours) &&
|
!isNaN(selectedHours) &&
|
||||||
@@ -82,9 +97,11 @@ const DaysSelector = ({
|
|||||||
// Basic validation for inputs
|
// Basic validation for inputs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let valid = true;
|
let valid = true;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selectedDays < 0 ||
|
selectedDays < 0 ||
|
||||||
(maxDays && selectedDays > maxDays) ||
|
selectedDays > 7 ||
|
||||||
|
selectedDays > maxDays ||
|
||||||
isNaN(selectedDays)
|
isNaN(selectedDays)
|
||||||
) {
|
) {
|
||||||
valid = false;
|
valid = false;
|
||||||
@@ -98,12 +115,16 @@ const DaysSelector = ({
|
|||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (selectedDays === maxDays) {
|
||||||
maxDays &&
|
if (selectedHours > maxHours) {
|
||||||
selectedDays === maxDays &&
|
valid = false;
|
||||||
(selectedHours !== 0 || selectedMinutes !== 0)
|
}
|
||||||
) {
|
|
||||||
valid = false;
|
if (selectedHours === maxHours) {
|
||||||
|
if (selectedMinutes > maxMinutes) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedDays <= 0 && selectedHours <= 0 && selectedMinutes <= 0) {
|
if (selectedDays <= 0 && selectedHours <= 0 && selectedMinutes <= 0) {
|
||||||
@@ -114,6 +135,8 @@ const DaysSelector = ({
|
|||||||
}, [
|
}, [
|
||||||
dateSelected,
|
dateSelected,
|
||||||
maxDays,
|
maxDays,
|
||||||
|
maxHours,
|
||||||
|
maxMinutes,
|
||||||
onChange,
|
onChange,
|
||||||
selectedDays,
|
selectedDays,
|
||||||
selectedHours,
|
selectedHours,
|
||||||
@@ -165,7 +188,7 @@ const DaysSelector = ({
|
|||||||
className={`reverseInput removeArrows`}
|
className={`reverseInput removeArrows`}
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max={maxDays ? maxDays.toString() : "999"}
|
max="7"
|
||||||
label="Days"
|
label="Days"
|
||||||
name={id}
|
name={id}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
BucketVersioningResponse,
|
BucketVersioningResponse,
|
||||||
GetBucketRetentionConfig,
|
GetBucketRetentionConfig,
|
||||||
} from "api/consoleApi";
|
} from "api/consoleApi";
|
||||||
|
import { AppState } from "store";
|
||||||
|
|
||||||
const defaultRewind = {
|
const defaultRewind = {
|
||||||
rewindEnabled: false,
|
rewindEnabled: false,
|
||||||
@@ -76,6 +77,7 @@ const initialState: ObjectBrowserState = {
|
|||||||
validity: 0,
|
validity: 0,
|
||||||
},
|
},
|
||||||
longFileOpen: false,
|
longFileOpen: false,
|
||||||
|
maxShareLinkExpTime: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const objectBrowserSlice = createSlice({
|
export const objectBrowserSlice = createSlice({
|
||||||
@@ -371,6 +373,9 @@ export const objectBrowserSlice = createSlice({
|
|||||||
setAnonymousAccessOpen: (state, action: PayloadAction<boolean>) => {
|
setAnonymousAccessOpen: (state, action: PayloadAction<boolean>) => {
|
||||||
state.anonymousAccessOpen = action.payload;
|
state.anonymousAccessOpen = action.payload;
|
||||||
},
|
},
|
||||||
|
setMaxShareLinkExpTime: (state, action: PayloadAction<number>) => {
|
||||||
|
state.maxShareLinkExpTime = action.payload;
|
||||||
|
},
|
||||||
errorInConnection: (state, action: PayloadAction<boolean>) => {
|
errorInConnection: (state, action: PayloadAction<boolean>) => {
|
||||||
state.connectionError = action.payload;
|
state.connectionError = action.payload;
|
||||||
if (action.payload) {
|
if (action.payload) {
|
||||||
@@ -425,7 +430,11 @@ export const {
|
|||||||
setSelectedBucket,
|
setSelectedBucket,
|
||||||
setLongFileOpen,
|
setLongFileOpen,
|
||||||
setAnonymousAccessOpen,
|
setAnonymousAccessOpen,
|
||||||
|
setMaxShareLinkExpTime,
|
||||||
errorInConnection,
|
errorInConnection,
|
||||||
} = objectBrowserSlice.actions;
|
} = objectBrowserSlice.actions;
|
||||||
|
|
||||||
|
export const maxShareLinkExpTime = (state: AppState) =>
|
||||||
|
state.objectBrowser.maxShareLinkExpTime;
|
||||||
|
|
||||||
export default objectBrowserSlice.reducer;
|
export default objectBrowserSlice.reducer;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
failObject,
|
failObject,
|
||||||
setAnonymousAccessOpen,
|
setAnonymousAccessOpen,
|
||||||
setDownloadRenameModal,
|
setDownloadRenameModal,
|
||||||
|
setMaxShareLinkExpTime,
|
||||||
setNewObject,
|
setNewObject,
|
||||||
setPreviewOpen,
|
setPreviewOpen,
|
||||||
setSelectedPreview,
|
setSelectedPreview,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
} from "./objectBrowserSlice";
|
} from "./objectBrowserSlice";
|
||||||
import { setSnackBarMessage } from "../../../systemSlice";
|
import { setSnackBarMessage } from "../../../systemSlice";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
import { api } from "api";
|
||||||
|
|
||||||
export const downloadSelected = createAsyncThunk(
|
export const downloadSelected = createAsyncThunk(
|
||||||
"objectBrowser/downloadSelected",
|
"objectBrowser/downloadSelected",
|
||||||
@@ -203,3 +205,17 @@ export const openAnonymousAccess = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getMaxShareLinkExpTime = createAsyncThunk(
|
||||||
|
"objectBrowser/maxShareLinkExpTime",
|
||||||
|
async (_, { rejectWithValue, dispatch }) => {
|
||||||
|
return api.buckets
|
||||||
|
.getMaxShareLinkExp()
|
||||||
|
.then((res) => {
|
||||||
|
dispatch(setMaxShareLinkExpTime(res.data.exp));
|
||||||
|
})
|
||||||
|
.catch(async (res) => {
|
||||||
|
return rejectWithValue(res.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export interface ObjectBrowserState {
|
|||||||
longFileOpen: boolean;
|
longFileOpen: boolean;
|
||||||
anonymousAccessOpen: boolean;
|
anonymousAccessOpen: boolean;
|
||||||
connectionError: boolean;
|
connectionError: boolean;
|
||||||
|
maxShareLinkExpTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObjectManager {
|
export interface ObjectManager {
|
||||||
|
|||||||
Reference in New Issue
Block a user