Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export default function App() {
const environment = useSelector(selectCurrentEnvironment);
const namespace = useSelector(selectCurrentNamespace);

let title = `Flipt | ${environment.key} · ${namespace.name}`;
const title = `Flipt | ${environment.key} · ${namespace.name}`;

return (
<>
Expand Down
47 changes: 23 additions & 24 deletions ui/src/app/analytics/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ export default function Analytics() {
durations[0]
);
const [pollingInterval, setPollingInterval] = useState<number>(0);
const [prevSelection, setPrevSelection] = useState({
selectedDuration,
selectedFlag
});

if (
prevSelection.selectedDuration !== selectedDuration ||
prevSelection.selectedFlag !== selectedFlag
) {
// Set the polling interval to 0 every time we change durations or flag
setPrevSelection({ selectedDuration, selectedFlag });
setPollingInterval(0);
}

// Select flag from path param if present
useEffect(() => {
Expand All @@ -119,31 +132,22 @@ export default function Analytics() {
}
}, [flagKey, flagOptions, selectedFlag]);

// Keep selectedFlag in sync with flagOptions
useEffect(() => {
if (
flagOptions.length > 0 &&
selectedFlag &&
!flagOptions.find((f) => f.key === selectedFlag.key)
) {
setSelectedFlag(null);
}
if (flagOptions.length === 0) {
setSelectedFlag(null);
}
}, [flagsData, flagOptions, selectedFlag]);
const effectiveSelectedFlag = useMemo(() => {
if (!selectedFlag || flagOptions.length === 0) return null;
return flagOptions.find((f) => f.key === selectedFlag.key) ?? null;
}, [flagOptions, selectedFlag]);

// Analytics query
const d = new Date();
d.setSeconds(0);
d.setMilliseconds(0);

const getFlagEvaluationCount = useGetFlagEvaluationCountQuery(
selectedFlag && info.analytics?.enabled
effectiveSelectedFlag && info.analytics?.enabled
? {
environmentKey: environment.key,
namespaceKey: namespace.key,
flagKey: selectedFlag.key,
flagKey: effectiveSelectedFlag.key,
from: addMinutes(
d,
selectedDuration?.value ? selectedDuration.value * -1 : -60
Expand All @@ -153,7 +157,7 @@ export default function Analytics() {
: skipToken,
{
pollingInterval,
skip: !info.analytics?.enabled || !selectedFlag
skip: !info.analytics?.enabled || !effectiveSelectedFlag
}
);

Expand All @@ -164,11 +168,6 @@ export default function Analytics() {
};
}, [getFlagEvaluationCount]);

// Set the polling interval to 0 every time we change durations or flag
useEffect(() => {
setPollingInterval(0);
}, [selectedDuration, selectedFlag]);

// Empty state if analytics disabled
if (!info.analytics?.enabled) {
return (
Expand Down Expand Up @@ -216,7 +215,7 @@ export default function Analytics() {
className="w-lg"
placeholder="Select or search for a flag"
values={flagOptions}
selected={selectedFlag}
selected={effectiveSelectedFlag}
setSelected={(flag) => {
setSelectedFlag(flag);
if (flag) {
Expand Down Expand Up @@ -275,7 +274,7 @@ export default function Analytics() {
</Button>
</Well>
</div>
) : !selectedFlag ? (
) : !effectiveSelectedFlag ? (
<div className="mt-12 w-full">
<Well>
<ChartNoAxesCombinedIcon className="h-12 w-12 text-muted-foreground/30 mb-4" />
Expand All @@ -292,7 +291,7 @@ export default function Analytics() {
<Graph
timestamps={flagEvaluationCount.timestamps}
values={flagEvaluationCount.values}
flagKey={selectedFlag.key}
flagKey={effectiveSelectedFlag.key}
/>
</div>
)}
Expand Down
3 changes: 2 additions & 1 deletion ui/src/app/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Toaster } from '~/components/Sonner';
import { IAuthMethod } from '~/types/Auth';

import logoFlag from '~/assets/logo-flag.png';
import { browser } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSession } from '~/data/hooks/session';
import { upperFirst } from '~/utils/helpers';
Expand Down Expand Up @@ -70,7 +71,7 @@ function InnerLoginButtons() {
}
clearError();
const body = await res.json();
window.location.href = body.authorizeUrl;
browser.navigateTo(body.authorizeUrl);
};
const {
data: listAuthProviders,
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/auth/authApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const authProvidersApi = createApi({
query: () => {
return { url: '/method' };
},
providesTags: (_result, _error, _args) => [{ type: 'Provider' }]
providesTags: () => [{ type: 'Provider' }]
})
})
});
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/environments/environmentsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const selectEnvironments = createSelector(
[(state: RootState) => state.environments.environments],
(environments) => {
return Object.entries(environments)
.map(([_, value]) => value)
.map(([, value]) => value)
.filter((env) => env.configuration?.base === undefined) as IEnvironment[]; // ignore branched environments
}
);
Expand All @@ -95,7 +95,7 @@ export const selectAllEnvironments = createSelector(
[(state: RootState) => state.environments.environments],
(environments) => {
return Object.entries(environments).map(
([_, value]) => value
([, value]) => value
) as IEnvironment[];
}
);
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/flags/Flag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
} from './flagsApi';

export default function Flag() {
let { flagKey } = useParams();
const { flagKey } = useParams();

const [showDeleteFlagModal, setShowDeleteFlagModal] = useState(false);
const [showCopyFlagModal, setShowCopyFlagModal] = useState(false);
Expand Down
5 changes: 1 addition & 4 deletions ui/src/app/namespaces/namespacesApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { createSelector, createSlice } from '@reduxjs/toolkit';
import { createApi } from '@reduxjs/toolkit/query/react';

Expand Down Expand Up @@ -47,9 +46,7 @@ export const { currentNamespaceChanged } = namespacesSlice.actions;
export const selectNamespaces = createSelector(
[(state: RootState) => state.namespaces.namespaces],
(namespaces) => {
return Object.entries(namespaces).map(
([_, value]) => value
) as INamespace[];
return Object.entries(namespaces).map(([, value]) => value) as INamespace[];
}
);

Expand Down
18 changes: 9 additions & 9 deletions ui/src/app/preferences/Preferences.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Formik } from 'formik';
import { Clock, Moon, Radio, Sun } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { Switch } from '~/components/Switch';
Expand Down Expand Up @@ -41,17 +41,13 @@ export default function Preferences() {
const { inTimezone } = useTimezone();
const isUTC = useMemo(() => timezone === Timezone.UTC, [timezone]);

const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const DEBOUNCE_DELAY = 1000;

const [isInitialLoad, setIsInitialLoad] = useState(true);
const isInitialLoad = useRef(true);

useEffect(() => {
setIsInitialLoad(false);
}, []);

useEffect(() => {
if (lastSaved && !isInitialLoad) {
if (lastSaved && !isInitialLoad.current) {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
Expand All @@ -64,7 +60,7 @@ export default function Preferences() {
dispatch(resetLastSaved());
debounceTimerRef.current = null;
}, DEBOUNCE_DELAY);
} else if (lastSaved && isInitialLoad) {
} else if (lastSaved && isInitialLoad.current) {
dispatch(resetLastSaved());
}

Expand All @@ -75,6 +71,10 @@ export default function Preferences() {
};
}, [lastSaved, dispatch, setNotification, isInitialLoad]);

useEffect(() => {
isInitialLoad.current = false;
}, []);

return (
<Formik initialValues={initialValues} onSubmit={() => {}}>
<div className="my-6">
Expand Down
5 changes: 2 additions & 3 deletions ui/src/app/preferences/preferencesSlice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { PayloadAction, createSlice } from '@reduxjs/toolkit';

import { fetchInfoAsync } from '~/app/meta/metaSlice';
Expand Down Expand Up @@ -58,7 +57,7 @@ const getInitialState = (): PreferencesState => {
realtime = preferences.realtime;
}
}
} catch (e) {
} catch (_e) {
// localStorage is disabled or not available, ignore
}

Expand Down Expand Up @@ -86,7 +85,7 @@ const savePreferences = (state: PreferencesState) => {
})
);
state.lastSaved = Date.now();
} catch (e) {
} catch (_e) {
// localStorage is disabled or not available, ignore
}
};
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/segments/Segment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function ReferencedFlags({
}

export default function Segment() {
let { segmentKey } = useParams();
const { segmentKey } = useParams();

const [showDeleteSegmentModal, setShowDeleteSegmentModal] =
useState<boolean>(false);
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/NavUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {

import { User } from '~/types/auth/User';

import { revokeAuthSelf } from '~/data/api';
import { browser, revokeAuthSelf } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSession } from '~/data/hooks/session';
import { redirectAfterLogout } from '~/utils/navigation';
Expand All @@ -37,7 +37,7 @@ export function NavUser({ user }: { user: User }) {
redirectAfterLogout(
(next: string, hard: boolean) => {
if (hard) {
window.location.href = next;
browser.navigateTo(next);
} else {
navigate(next);
}
Expand Down
60 changes: 30 additions & 30 deletions ui/src/components/constraints/ConstraintTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,31 @@ import { useTimezone } from '~/data/hooks/timezone';

// Component for displaying array values in constraints
function ConstraintArrayValue({ value }: { value: string | undefined }) {
let items: string[] | number[];
try {
const items: string[] | number[] = JSON.parse(value || '[]');
return (
<div className="max-w-[300px]">
<div className="flex flex-wrap gap-1">
{items.slice(0, 5).map((item, idx) => (
<span
key={idx}
className="rounded-full bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs"
>
{String(item)}
</span>
))}
{items.length > 5 && (
<span className="rounded-full bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs">
+{items.length - 5} more
</span>
)}
</div>
</div>
);
} catch (err) {
items = JSON.parse(value || '[]');
} catch {
return <span className="text-red-500">Invalid array</span>;
}
return (
<div className="max-w-[300px]">
<div className="flex flex-wrap gap-1">
{items.slice(0, 5).map((item, idx) => (
<span
key={idx}
className="rounded-full bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs"
>
{String(item)}
</span>
))}
{items.length > 5 && (
<span className="rounded-full bg-gray-100 dark:bg-gray-800 px-2 py-0.5 text-xs">
+{items.length - 5} more
</span>
)}
</div>
</div>
);
}

// Component for displaying constraint values based on type
Expand All @@ -67,22 +68,21 @@ function ConstraintValue({ constraint }: { constraint: IConstraint }) {
constraint.type === ConstraintType.DATETIME &&
constraint.value !== undefined
) {
let formattedDate: string;
try {
// Attempt to format the date - if it fails, show a fallback
const formattedDate = inTimezone(constraint.value);
return (
<span className="text-sm text-gray-900 dark:text-white">
{formattedDate}
</span>
);
} catch (err) {
// Show the raw value with an error indication
formattedDate = inTimezone(constraint.value);
} catch {
return (
<span className="text-sm text-red-600 dark:text-red-400">
{constraint.value || '(invalid date)'}
</span>
);
}
return (
<span className="text-sm text-gray-900 dark:text-white">
{formattedDate}
</span>
);
}

if (isArrayValue) {
Expand Down
Loading
Loading