diff --git a/portal-ui/src/common/utils.ts b/portal-ui/src/common/utils.ts index 7052d9bbd..d64955653 100644 --- a/portal-ui/src/common/utils.ts +++ b/portal-ui/src/common/utils.ts @@ -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": diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/bucketDetailsSlice.ts b/portal-ui/src/screens/Console/Buckets/BucketDetails/bucketDetailsSlice.ts index f46de9f6a..db55c7508 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/bucketDetailsSlice.ts +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/bucketDetailsSlice.ts @@ -31,7 +31,7 @@ const initialState: BucketDetailsState = { }; export const bucketDetailsSlice = createSlice({ - name: "trace", + name: "bucketDetails", initialState, reducers: { setBucketDetailsTab: (state, action: PayloadAction) => { diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx index cd53da670..92d77193d 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ShareFile.tsx @@ -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(""); const [isLoadingVersion, setIsLoadingVersion] = useState(true); const [isLoadingFile, setIsLoadingFile] = useState(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. -
-
- 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.

+ You can increase the maximum configuration time by setting + the MINIO_STS_DURATION environment variable on all your + nodes.

+ You can use mc share as an alternative to this UI, + where the session length does not limit the URL validity. + + } + > + + The following URL lets you share this object without requiring + a login.
+ The URL expires automatically at the earlier of your + configured time ({niceTimeFromSeconds(maxshareLinkExpTimeVal)} + ) or the expiration of your current web session. +
+

@@ -184,7 +219,7 @@ const ShareFile = ({ initialDate={initialDate} id="date" label="Active for" - maxDays={7} + maxSeconds={maxshareLinkExpTimeVal} onChange={dateChanged} entity="Link" /> diff --git a/portal-ui/src/screens/Console/Common/FormComponents/DaysSelector/DaysSelector.tsx b/portal-ui/src/screens/Console/Common/FormComponents/DaysSelector/DaysSelector.tsx index 6993e03f0..54cd142b5 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/DaysSelector/DaysSelector.tsx +++ b/portal-ui/src/screens/Console/Common/FormComponents/DaysSelector/DaysSelector.tsx @@ -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(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(0); const [selectedHours, setSelectedHours] = useState(0); const [selectedMinutes, setSelectedMinutes] = useState(0); const [validDate, setValidDate] = useState(true); const [dateSelected, setDateSelected] = useState(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) => { diff --git a/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts b/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts index 29f5a9f45..6a7f838d0 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts @@ -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) => { state.anonymousAccessOpen = action.payload; }, + setMaxShareLinkExpTime: (state, action: PayloadAction) => { + state.maxShareLinkExpTime = action.payload; + }, errorInConnection: (state, action: PayloadAction) => { 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; diff --git a/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserThunks.ts b/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserThunks.ts index 9ddff6d95..d055d69af 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserThunks.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserThunks.ts @@ -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); + }); + }, +); diff --git a/portal-ui/src/screens/Console/ObjectBrowser/types.ts b/portal-ui/src/screens/Console/ObjectBrowser/types.ts index 8e7804985..db2f84fe5 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/types.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/types.ts @@ -57,6 +57,7 @@ export interface ObjectBrowserState { longFileOpen: boolean; anonymousAccessOpen: boolean; connectionError: boolean; + maxShareLinkExpTime: number; } export interface ObjectManager {