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
|
||||
export const niceDays = (secondsValue: string, timeVariant: string = "s") => {
|
||||
let seconds = parseFloat(secondsValue);
|
||||
@@ -258,6 +288,7 @@ export const niceDays = (secondsValue: string, timeVariant: string = "s") => {
|
||||
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") => {
|
||||
switch (timeVariant) {
|
||||
case "ns":
|
||||
|
||||
@@ -31,7 +31,7 @@ const initialState: BucketDetailsState = {
|
||||
};
|
||||
|
||||
export const bucketDetailsSlice = createSlice({
|
||||
name: "trace",
|
||||
name: "bucketDetails",
|
||||
initialState,
|
||||
reducers: {
|
||||
setBucketDetailsTab: (state, action: PayloadAction<string>) => {
|
||||
|
||||
@@ -16,11 +16,22 @@
|
||||
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
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 ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
|
||||
import DaysSelector from "../../../../Common/FormComponents/DaysSelector/DaysSelector";
|
||||
import { encodeURLString } from "../../../../../../common/utils";
|
||||
import {
|
||||
encodeURLString,
|
||||
niceTimeFromSeconds,
|
||||
} from "../../../../../../common/utils";
|
||||
import {
|
||||
selDistSet,
|
||||
setModalErrorSnackMessage,
|
||||
@@ -30,6 +41,8 @@ import { useAppDispatch } from "../../../../../../store";
|
||||
import { BucketObject } from "api/consoleApi";
|
||||
import { api } from "api";
|
||||
import { errorToHandler } from "api/errors";
|
||||
import { getMaxShareLinkExpTime } from "screens/Console/ObjectBrowser/objectBrowserThunks";
|
||||
import { maxShareLinkExpTime } from "screens/Console/ObjectBrowser/objectBrowserSlice";
|
||||
|
||||
interface IShareFileProps {
|
||||
open: boolean;
|
||||
@@ -46,6 +59,7 @@ const ShareFile = ({
|
||||
}: IShareFileProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const distributedSetup = useSelector(selDistSet);
|
||||
const maxshareLinkExpTimeVal = useSelector(maxShareLinkExpTime);
|
||||
const [shareURL, setShareURL] = useState<string>("");
|
||||
const [isLoadingVersion, setIsLoadingVersion] = useState<boolean>(true);
|
||||
const [isLoadingFile, setIsLoadingFile] = useState<boolean>(false);
|
||||
@@ -65,6 +79,10 @@ const ShareFile = ({
|
||||
setShareURL("");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getMaxShareLinkExpTime());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
// In case version is undefined, we get the latest version of the object
|
||||
if (dataObject.version_id === undefined) {
|
||||
@@ -172,11 +190,28 @@ const ShareFile = ({
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
This is a temporary URL with integrated access credentials for
|
||||
sharing objects valid for up to 7 days.
|
||||
<br />
|
||||
<br />
|
||||
The temporary URL expires after the configured time limit.
|
||||
<Tooltip
|
||||
placement="right"
|
||||
tooltip={
|
||||
<span>
|
||||
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>
|
||||
<br />
|
||||
<Grid item xs={12}>
|
||||
@@ -184,7 +219,7 @@ const ShareFile = ({
|
||||
initialDate={initialDate}
|
||||
id="date"
|
||||
label="Active for"
|
||||
maxDays={7}
|
||||
maxSeconds={maxshareLinkExpTimeVal}
|
||||
onChange={dateChanged}
|
||||
entity="Link"
|
||||
/>
|
||||
|
||||
@@ -18,10 +18,14 @@ import React, { useEffect, useState } from "react";
|
||||
import { DateTime } from "luxon";
|
||||
import { Box, InputBox, InputLabel, LinkIcon } from "mds";
|
||||
|
||||
const DAY_SECONDS = 86400;
|
||||
const HOUR_SECONDS = 3600;
|
||||
const HOUR_MINUTES = 60;
|
||||
|
||||
interface IDaysSelector {
|
||||
id: string;
|
||||
initialDate: Date;
|
||||
maxDays?: number;
|
||||
maxSeconds: number;
|
||||
label: string;
|
||||
entity: string;
|
||||
onChange: (newDate: string, isValid: boolean) => void;
|
||||
@@ -43,16 +47,27 @@ const DaysSelector = ({
|
||||
id,
|
||||
initialDate,
|
||||
label,
|
||||
maxDays,
|
||||
maxSeconds,
|
||||
entity,
|
||||
onChange,
|
||||
}: 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 [selectedMinutes, setSelectedMinutes] = useState<number>(0);
|
||||
const [validDate, setValidDate] = useState<boolean>(true);
|
||||
const [dateSelected, setDateSelected] = useState<DateTime>(DateTime.now());
|
||||
|
||||
// Set initial values
|
||||
useEffect(() => {
|
||||
setSelectedDays(maxDays);
|
||||
setSelectedHours(maxHours);
|
||||
setSelectedMinutes(maxMinutes);
|
||||
}, [maxDays, maxHours, maxMinutes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isNaN(selectedHours) &&
|
||||
@@ -82,9 +97,11 @@ const DaysSelector = ({
|
||||
// Basic validation for inputs
|
||||
useEffect(() => {
|
||||
let valid = true;
|
||||
|
||||
if (
|
||||
selectedDays < 0 ||
|
||||
(maxDays && selectedDays > maxDays) ||
|
||||
selectedDays > 7 ||
|
||||
selectedDays > maxDays ||
|
||||
isNaN(selectedDays)
|
||||
) {
|
||||
valid = false;
|
||||
@@ -98,12 +115,16 @@ const DaysSelector = ({
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (
|
||||
maxDays &&
|
||||
selectedDays === maxDays &&
|
||||
(selectedHours !== 0 || selectedMinutes !== 0)
|
||||
) {
|
||||
valid = false;
|
||||
if (selectedDays === maxDays) {
|
||||
if (selectedHours > maxHours) {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (selectedHours === maxHours) {
|
||||
if (selectedMinutes > maxMinutes) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedDays <= 0 && selectedHours <= 0 && selectedMinutes <= 0) {
|
||||
@@ -114,6 +135,8 @@ const DaysSelector = ({
|
||||
}, [
|
||||
dateSelected,
|
||||
maxDays,
|
||||
maxHours,
|
||||
maxMinutes,
|
||||
onChange,
|
||||
selectedDays,
|
||||
selectedHours,
|
||||
@@ -165,7 +188,7 @@ const DaysSelector = ({
|
||||
className={`reverseInput removeArrows`}
|
||||
type="number"
|
||||
min="0"
|
||||
max={maxDays ? maxDays.toString() : "999"}
|
||||
max="7"
|
||||
label="Days"
|
||||
name={id}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
BucketVersioningResponse,
|
||||
GetBucketRetentionConfig,
|
||||
} from "api/consoleApi";
|
||||
import { AppState } from "store";
|
||||
|
||||
const defaultRewind = {
|
||||
rewindEnabled: false,
|
||||
@@ -76,6 +77,7 @@ const initialState: ObjectBrowserState = {
|
||||
validity: 0,
|
||||
},
|
||||
longFileOpen: false,
|
||||
maxShareLinkExpTime: 0,
|
||||
};
|
||||
|
||||
export const objectBrowserSlice = createSlice({
|
||||
@@ -371,6 +373,9 @@ export const objectBrowserSlice = createSlice({
|
||||
setAnonymousAccessOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.anonymousAccessOpen = action.payload;
|
||||
},
|
||||
setMaxShareLinkExpTime: (state, action: PayloadAction<number>) => {
|
||||
state.maxShareLinkExpTime = action.payload;
|
||||
},
|
||||
errorInConnection: (state, action: PayloadAction<boolean>) => {
|
||||
state.connectionError = action.payload;
|
||||
if (action.payload) {
|
||||
@@ -425,7 +430,11 @@ export const {
|
||||
setSelectedBucket,
|
||||
setLongFileOpen,
|
||||
setAnonymousAccessOpen,
|
||||
setMaxShareLinkExpTime,
|
||||
errorInConnection,
|
||||
} = objectBrowserSlice.actions;
|
||||
|
||||
export const maxShareLinkExpTime = (state: AppState) =>
|
||||
state.objectBrowser.maxShareLinkExpTime;
|
||||
|
||||
export default objectBrowserSlice.reducer;
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
failObject,
|
||||
setAnonymousAccessOpen,
|
||||
setDownloadRenameModal,
|
||||
setMaxShareLinkExpTime,
|
||||
setNewObject,
|
||||
setPreviewOpen,
|
||||
setSelectedPreview,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
} from "./objectBrowserSlice";
|
||||
import { setSnackBarMessage } from "../../../systemSlice";
|
||||
import { DateTime } from "luxon";
|
||||
import { api } from "api";
|
||||
|
||||
export const downloadSelected = createAsyncThunk(
|
||||
"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;
|
||||
anonymousAccessOpen: boolean;
|
||||
connectionError: boolean;
|
||||
maxShareLinkExpTime: number;
|
||||
}
|
||||
|
||||
export interface ObjectManager {
|
||||
|
||||
Reference in New Issue
Block a user