- {!disableGlobalLabel && }
+ {!cannotFilterByLabel && (
+
+ )}
{!disableGlobalSort && }
@@ -212,6 +238,20 @@ const EnhancedTable = ({
)}
+ {/* RESET FILTERS */}
+ 0 || state.sortBy?.length > 0
+ ? 'visible'
+ : 'hidden'
+ }
+ >
+
+
+
+
+
+
{/* NO DATA MESSAGE */}
{!isLoading &&
@@ -245,15 +285,18 @@ const EnhancedTable = ({
original={original}
value={values}
className={isSelected ? 'selected' : ''}
- onClickLabel={(label) => {
- const currentFilter =
- state.filters
- ?.filter(({ id }) => id === LABEL_COLUMN_ID)
- ?.map(({ value }) => value) || []
+ {...(!cannotFilterByLabel && {
+ onClickLabel: (label) => {
+ const currentFilter =
+ state.filters
+ ?.filter(({ id }) => id === LABEL_COLUMN_ID)
+ ?.map(({ value }) => value)
+ ?.flat() || []
- const nextFilter = [...new Set([...currentFilter, label])]
- setFilter(LABEL_COLUMN_ID, nextFilter)
- }}
+ const nextFilter = [...new Set([...currentFilter, label])]
+ setFilter(LABEL_COLUMN_ID, nextFilter)
+ },
+ })}
onClick={() => {
typeof onRowClick === 'function' && onRowClick(original)
@@ -295,6 +338,7 @@ EnhancedTable.propTypes = {
disableGlobalSort: PropTypes.bool,
disableRowSelect: PropTypes.bool,
displaySelectedRows: PropTypes.bool,
+ useUpdateMutation: PropTypes.func,
onSelectedRowsChange: PropTypes.func,
onRowClick: PropTypes.func,
pageSize: PropTypes.number,
diff --git a/src/fireedge/src/client/components/Tables/Enhanced/styles.js b/src/fireedge/src/client/components/Tables/Enhanced/styles.js
index 6bc9e4ecef..76fb3dfda3 100644
--- a/src/fireedge/src/client/components/Tables/Enhanced/styles.js
+++ b/src/fireedge/src/client/components/Tables/Enhanced/styles.js
@@ -24,7 +24,7 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
},
toolbar: {
...typography.body1,
- marginBottom: 16,
+ marginBottom: '1em',
display: 'grid',
gridTemplateRows: 'auto auto',
gridTemplateAreas: `
@@ -55,6 +55,16 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
justifySelf: 'end',
gap: '1em',
},
+ resetFilters: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.5em',
+ cursor: 'pointer',
+ marginBottom: '1em',
+ '&:hover': {
+ color: palette.secondary.dark,
+ },
+ },
body: {
overflow: 'auto',
display: 'grid',
diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js
index c305329831..87e7bc04cc 100644
--- a/src/fireedge/src/client/constants/index.js
+++ b/src/fireedge/src/client/constants/index.js
@@ -59,6 +59,7 @@ export const SCHEMES = Setting.SCHEMES
export const DEFAULT_SCHEME = Setting.SCHEMES.SYSTEM
export const CURRENCY = SERVER_CONFIG?.currency ?? 'EUR'
+export const LOCALE = SERVER_CONFIG?.lang?.replace('_', '-') ?? undefined
export const DEFAULT_LANGUAGE = SERVER_CONFIG?.default_lang ?? 'en'
export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages`
export const LANGUAGES = SERVER_CONFIG.langs ?? {
diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js
index 760c3579b4..083f752b4f 100644
--- a/src/fireedge/src/client/constants/translates.js
+++ b/src/fireedge/src/client/constants/translates.js
@@ -34,6 +34,7 @@ module.exports = {
NumberOfResourcesSelected: 'All %s resources are selected',
SelectAllResources: 'Select all %s resources',
ClearSelection: 'Clear selection',
+ ResetFilters: 'Clear current search query, filters, and sorts',
/* actions */
Accept: 'Accept',
@@ -301,6 +302,8 @@ module.exports = {
Labels: 'Labels',
NewLabelOrSearch: 'New label or search',
LabelAlreadyExists: 'Label already exists',
+ PressToCreateLabel: 'Press enter to create a new label',
+ SavesInTheUserTemplate: "Saved in the User's template",
/* sections - system */
User: 'User',
diff --git a/src/fireedge/src/client/containers/Hosts/index.js b/src/fireedge/src/client/containers/Hosts/index.js
index 5b10b9e0e1..302051878b 100644
--- a/src/fireedge/src/client/containers/Hosts/index.js
+++ b/src/fireedge/src/client/containers/Hosts/index.js
@@ -15,18 +15,24 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
-import { BookmarkEmpty } from 'iconoir-react'
-import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
+import GotoIcon from 'iconoir-react/dist/Pin'
+import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
+import Cancel from 'iconoir-react/dist/Cancel'
+import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
-import hostApi from 'client/features/OneApi/host'
+import {
+ useLazyGetHostQuery,
+ useUpdateHostMutation,
+} from 'client/features/OneApi/host'
import { HostsTable } from 'client/components/Tables'
import HostTabs from 'client/components/Tabs/Host'
import HostActions from 'client/components/Tables/Hosts/actions'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
+import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
-import { T } from 'client/constants'
+import { T, Host } from 'client/constants'
/**
* Displays a list of Hosts with a split pane between the list and selected row(s).
@@ -47,6 +53,7 @@ function Hosts() {
{hasSelectedRows && (
@@ -56,8 +63,9 @@ function Hosts() {
) : (
selectedRows[0]?.toggleRowSelected(false)}
/>
)}
>
@@ -71,26 +79,45 @@ function Hosts() {
/**
* Displays details of a Host.
*
- * @param {string} id - Host id to display
+ * @param {Host} host - Host to display
* @param {Function} [gotoPage] - Function to navigate to a page of a Host
+ * @param {Function} [unselect] - Function to unselect a Host
* @returns {ReactElement} Host details
*/
-const InfoTabs = memo(({ id, gotoPage }) => {
- const host = hostApi.endpoints.getHosts.useQueryState(undefined, {
- selectFromResult: ({ data = [] }) => data.find((item) => +item.ID === +id),
- })
+const InfoTabs = memo(({ host, gotoPage, unselect }) => {
+ const [getVm, { data: lazyData, isFetching }] = useLazyGetHostQuery()
+ const id = lazyData?.ID ?? host.ID
+ const name = lazyData?.NAME ?? host.NAME
return (
-
- {`#${id} | ${host.NAME}`}
-
- {gotoPage && (
-
-
-
+ }
+ tooltip={Tr(T.Refresh)}
+ isSubmitting={isFetching}
+ onClick={() => getVm({ id })}
+ />
+ {typeof gotoPage === 'function' && (
+ }
+ tooltip={Tr(T.LocateOnTable)}
+ onClick={() => gotoPage()}
+ />
)}
+ {typeof unselect === 'function' && (
+ }
+ tooltip={Tr(T.Close)}
+ onClick={() => unselect()}
+ />
+ )}
+
+ {`#${id} | ${name}`}
+
@@ -98,8 +125,9 @@ const InfoTabs = memo(({ id, gotoPage }) => {
})
InfoTabs.propTypes = {
- id: PropTypes.string.isRequired,
+ host: PropTypes.object,
gotoPage: PropTypes.func,
+ unselect: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
diff --git a/src/fireedge/src/client/containers/MarketplaceApps/index.js b/src/fireedge/src/client/containers/MarketplaceApps/index.js
index 10a647f266..fc900dd9c8 100644
--- a/src/fireedge/src/client/containers/MarketplaceApps/index.js
+++ b/src/fireedge/src/client/containers/MarketplaceApps/index.js
@@ -15,18 +15,24 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
-import { BookmarkEmpty } from 'iconoir-react'
-import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
+import GotoIcon from 'iconoir-react/dist/Pin'
+import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
+import Cancel from 'iconoir-react/dist/Cancel'
+import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
-import marketAppApi from 'client/features/OneApi/marketplaceApp'
+import {
+ useUpdateAppMutation,
+ useLazyGetMarketplaceAppQuery,
+} from 'client/features/OneApi/marketplaceApp'
import { MarketplaceAppsTable } from 'client/components/Tables'
import MarketplaceAppActions from 'client/components/Tables/MarketplaceApps/actions'
import MarketplaceAppsTabs from 'client/components/Tabs/MarketplaceApp'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
+import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
-import { T } from 'client/constants'
+import { T, MarketplaceApp } from 'client/constants'
/**
* Displays a list of Marketplace Apps with a split pane between the list and selected row(s).
@@ -47,6 +53,7 @@ function MarketplaceApps() {
{hasSelectedRows && (
@@ -56,8 +63,9 @@ function MarketplaceApps() {
) : (
selectedRows[0]?.toggleRowSelected(false)}
/>
)}
>
@@ -71,30 +79,47 @@ function MarketplaceApps() {
/**
* Displays details of a Marketplace App.
*
- * @param {string} id - Marketplace App id to display
+ * @param {MarketplaceApp} app - Marketplace App to display
* @param {Function} [gotoPage] - Function to navigate to a page of a Marketplace App
+ * @param {Function} [unselect] - Function to unselect a Marketplace App
* @returns {ReactElement} Marketplace App details
*/
-const InfoTabs = memo(({ id, gotoPage }) => {
- const app = marketAppApi.endpoints.getMarketplaceApps.useQueryState(
- undefined,
- {
- selectFromResult: ({ data = [] }) =>
- data.find((item) => +item.ID === +id),
- }
- )
+const InfoTabs = memo(({ app, gotoPage, unselect }) => {
+ const [getApp, queryState] = useLazyGetMarketplaceAppQuery()
+ const { data: lazyData, isFetching } = queryState
+
+ const id = lazyData?.ID ?? app.ID
+ const name = lazyData?.NAME ?? app.NAME
return (
-
- {`#${id} | ${app.NAME}`}
-
- {gotoPage && (
-
-
-
+ }
+ tooltip={Tr(T.Refresh)}
+ isSubmitting={isFetching}
+ onClick={() => getApp({ id })}
+ />
+ {typeof gotoPage === 'function' && (
+ }
+ tooltip={Tr(T.LocateOnTable)}
+ onClick={() => gotoPage()}
+ />
)}
+ {typeof unselect === 'function' && (
+ }
+ tooltip={Tr(T.Close)}
+ onClick={() => unselect()}
+ />
+ )}
+
+ {`#${id} | ${name}`}
+
@@ -102,8 +127,9 @@ const InfoTabs = memo(({ id, gotoPage }) => {
})
InfoTabs.propTypes = {
- id: PropTypes.string.isRequired,
+ app: PropTypes.object,
gotoPage: PropTypes.func,
+ unselect: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
diff --git a/src/fireedge/src/client/containers/Settings/LabelsSection/index.js b/src/fireedge/src/client/containers/Settings/LabelsSection/index.js
index 5badfeba87..20a7a2116a 100644
--- a/src/fireedge/src/client/containers/Settings/LabelsSection/index.js
+++ b/src/fireedge/src/client/containers/Settings/LabelsSection/index.js
@@ -13,61 +13,78 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
-import { ReactElement, useMemo, useCallback } from 'react'
+import { ReactElement, useEffect, useCallback } from 'react'
import TrashIcon from 'iconoir-react/dist/Trash'
-import { Paper, Stack, Box, Typography, TextField } from '@mui/material'
+import { styled, Paper, Stack, Box, Typography, TextField } from '@mui/material'
import CircularProgress from '@mui/material/CircularProgress'
import { useForm } from 'react-hook-form'
+import {
+ useAddLabelMutation,
+ useRemoveLabelMutation,
+} from 'client/features/OneApi/auth'
import { useAuth } from 'client/features/Auth'
-import { useUpdateUserMutation } from 'client/features/OneApi/user'
import { useGeneralApi } from 'client/features/General'
import { useSearch } from 'client/hooks'
import { StatusChip } from 'client/components/Status'
import { SubmitButton } from 'client/components/FormControl'
-import { jsonToXml, getColorFromString } from 'client/models/Helper'
+import { getColorFromString } from 'client/models/Helper'
import { Translate, Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const NEW_LABEL_ID = 'new-label'
+const LabelWrapper = styled(Box)(({ theme, ownerState }) => ({
+ display: 'flex',
+ direction: 'row',
+ alignItems: 'center',
+ paddingInline: '0.5rem',
+ borderRadius: theme.shape.borderRadius * 2,
+ animation: ownerState.highlight ? 'highlight 2s ease-in-out' : undefined,
+ '@keyframes highlight': {
+ from: { backgroundColor: 'yellow' },
+ to: { backgroundColor: 'transparent' },
+ },
+}))
+
/**
* Section to change labels.
*
* @returns {ReactElement} Settings configuration UI
*/
const Settings = () => {
- const { user, settings } = useAuth()
+ const { labels } = useAuth()
const { enqueueError } = useGeneralApi()
- const [updateUser, { isLoading }] = useUpdateUserMutation()
-
- const currentLabels = useMemo(
- () => settings?.LABELS?.split(',').filter(Boolean) ?? [],
- [settings?.LABELS]
- )
+ const [removeLabel, { isLoading: removeLoading }] = useRemoveLabelMutation()
+ const [addLabel, { isLoading, data, isSuccess }] = useAddLabelMutation()
const { handleSubmit, register, reset, setFocus } = useForm({
reValidateMode: 'onSubmit',
})
const { result, handleChange } = useSearch({
- list: currentLabels,
- listOptions: { distance: 50 },
- wait: 500,
+ list: labels,
+ listOptions: { threshold: 0.2 },
+ wait: 400,
condition: !isLoading,
})
+ useEffect(() => {
+ if (!isSuccess) return
+
+ setTimeout(() => {
+ // scroll to the new label (if it exists)
+ document
+ ?.querySelector(`[data-cy='${data}']`)
+ ?.scrollIntoView({ behavior: 'smooth', block: 'center' })
+ }, 450)
+ }, [isSuccess])
+
const handleAddLabel = useCallback(
- async (newLabel) => {
+ async (formData) => {
try {
- const exists = currentLabels.some((label) => label === newLabel)
-
- if (exists) throw new Error(T.LabelAlreadyExists)
-
- const newLabels = currentLabels.concat(newLabel).join()
- const template = jsonToXml({ LABELS: newLabels })
- await updateUser({ id: user.ID, template, replace: 1 })
+ await addLabel({ newLabel: formData[NEW_LABEL_ID] }).unwrap()
} catch (error) {
enqueueError(error.message ?? T.SomethingWrong)
} finally {
@@ -77,42 +94,22 @@ const Settings = () => {
setFocus(NEW_LABEL_ID)
}
},
- [updateUser, currentLabels, handleChange, reset]
+ [addLabel, handleChange, reset]
)
const handleDeleteLabel = useCallback(
async (label) => {
try {
- const newLabels = currentLabels.filter((l) => l !== label).join()
- const template = jsonToXml({ LABELS: newLabels })
- await updateUser({ id: user.ID, template, replace: 1 })
-
- // Reset the search after deleting the label
- handleChange()
- } catch {
- enqueueError(T.SomethingWrong)
+ await removeLabel({ label }).unwrap()
+ } catch (error) {
+ enqueueError(error.message ?? T.SomethingWrong)
}
},
- [updateUser, currentLabels, handleChange]
+ [removeLabel, handleChange]
)
const handleKeyDown = useCallback(
- (evt) => {
- if (evt.key !== 'Enter') return
-
- handleSubmit(async (formData) => {
- const newLabel = formData[NEW_LABEL_ID]
-
- if (newLabel) await handleAddLabel(newLabel)
-
- // scroll to the new label (if it exists)
- setTimeout(() => {
- document
- ?.querySelector(`[data-cy='${newLabel}']`)
- ?.scrollIntoView({ behavior: 'smooth', block: 'center' })
- }, 500)
- })(evt)
- },
+ (evt) => evt.key === 'Enter' && handleSubmit(handleAddLabel)(evt),
[handleAddLabel, handleSubmit]
)
@@ -123,11 +120,16 @@ const Settings = () => {
-
+
{result?.map((label) => (
-
-
+
+
{
handleDeleteLabel(label)}
icon={}
/>
-
+
))}
{
) : undefined,
}}
{...register(NEW_LABEL_ID, { onChange: handleChange })}
- helperText={'Press enter to create a new label'}
+ helperText={Tr(T.PressToCreateLabel)}
/>
)
diff --git a/src/fireedge/src/client/containers/Settings/index.js b/src/fireedge/src/client/containers/Settings/index.js
index e5c83da1dd..22b2c1ec7e 100644
--- a/src/fireedge/src/client/containers/Settings/index.js
+++ b/src/fireedge/src/client/containers/Settings/index.js
@@ -34,7 +34,7 @@ const Settings = () => (
diff --git a/src/fireedge/src/client/containers/VirtualMachines/index.js b/src/fireedge/src/client/containers/VirtualMachines/index.js
index c2e1738593..fca7058349 100644
--- a/src/fireedge/src/client/containers/VirtualMachines/index.js
+++ b/src/fireedge/src/client/containers/VirtualMachines/index.js
@@ -15,11 +15,16 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useState, memo } from 'react'
import PropTypes from 'prop-types'
-import { Pin as GotoIcon, RefreshDouble, Cancel } from 'iconoir-react'
+import GotoIcon from 'iconoir-react/dist/Pin'
+import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
+import Cancel from 'iconoir-react/dist/Cancel'
import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
-import { useLazyGetVmQuery } from 'client/features/OneApi/vm'
+import {
+ useLazyGetVmQuery,
+ useUpdateUserTemplateMutation,
+} from 'client/features/OneApi/vm'
import { VmsTable } from 'client/components/Tables'
import VmActions from 'client/components/Tables/Vms/actions'
import VmTabs from 'client/components/Tabs/Vm'
@@ -48,6 +53,7 @@ function VirtualMachines() {
{hasSelectedRows && (
diff --git a/src/fireedge/src/client/containers/VmTemplates/index.js b/src/fireedge/src/client/containers/VmTemplates/index.js
index d79c24ed49..b97c058a7b 100644
--- a/src/fireedge/src/client/containers/VmTemplates/index.js
+++ b/src/fireedge/src/client/containers/VmTemplates/index.js
@@ -19,7 +19,10 @@ import { Pin as GotoIcon, RefreshDouble, Cancel } from 'iconoir-react'
import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
-import { useLazyGetTemplateQuery } from 'client/features/OneApi/vmTemplate'
+import {
+ useLazyGetTemplateQuery,
+ useUpdateTemplateMutation,
+} from 'client/features/OneApi/vmTemplate'
import { VmTemplatesTable } from 'client/components/Tables'
import VmTemplateActions from 'client/components/Tables/VmTemplates/actions'
import VmTemplateTabs from 'client/components/Tabs/VmTemplate'
@@ -48,6 +51,7 @@ function VmTemplates() {
{hasSelectedRows && (
diff --git a/src/fireedge/src/client/features/Auth/hooks.js b/src/fireedge/src/client/features/Auth/hooks.js
index 3635a427e6..cd2b53cb9a 100644
--- a/src/fireedge/src/client/features/Auth/hooks.js
+++ b/src/fireedge/src/client/features/Auth/hooks.js
@@ -22,6 +22,7 @@ import { name as authSlice, actions, logout } from 'client/features/Auth/slice'
import groupApi from 'client/features/OneApi/group'
import systemApi from 'client/features/OneApi/system'
import { ResourceView } from 'client/apps/sunstone/routes'
+import { areStringEqual } from 'client/models/Helper'
import {
_APPS,
RESOURCE_NAMES,
@@ -61,6 +62,15 @@ export const useAuth = () => {
}
)
+ const userLabels = useMemo(() => {
+ const labels = user?.TEMPLATE?.LABELS?.split(',') ?? []
+
+ return labels
+ .filter(Boolean)
+ .map((label) => label.toUpperCase())
+ .sort(areStringEqual({ numeric: true, ignorePunctuation: true }))
+ }, [user?.TEMPLATE?.LABELS])
+
return useMemo(
() => ({
...auth,
@@ -75,6 +85,7 @@ export const useAuth = () => {
...(user?.TEMPLATE ?? {}),
...(user?.TEMPLATE?.FIREEDGE ?? {}),
},
+ labels: userLabels ?? [],
isLogged:
!!jwt &&
!!user &&
diff --git a/src/fireedge/src/client/features/OneApi/auth.js b/src/fireedge/src/client/features/OneApi/auth.js
index a7a328278f..be7b97ad8b 100644
--- a/src/fireedge/src/client/features/OneApi/auth.js
+++ b/src/fireedge/src/client/features/OneApi/auth.js
@@ -20,6 +20,7 @@ import { dismissSnackbar } from 'client/features/General/actions'
import { oneApi, ONE_RESOURCES_POOL } from 'client/features/OneApi'
import userApi from 'client/features/OneApi/user'
+import { jsonToXml } from 'client/models/Helper'
import { storage } from 'client/utils'
import { JWT_NAME, FILTER_POOL, ONEADMIN_ID } from 'client/constants'
@@ -88,8 +89,8 @@ const authApi = oneApi.injectEndpoints({
}),
changeAuthGroup: builder.mutation({
/**
- * @param {object} data - User credentials
- * @param {string} data.group - Group id
+ * @param {object} params - Request parameters
+ * @param {string} params.group - Group id
* @returns {Promise} Response data from request
* @throws Fails when response isn't code 200
*/
@@ -118,6 +119,67 @@ const authApi = oneApi.injectEndpoints({
},
invalidatesTags: [...Object.values(restOfPool)],
}),
+ addLabel: builder.mutation({
+ /**
+ * @param {object} params - Request parameters
+ * @param {string} params.newLabel - Label to add
+ * @returns {Promise} Response data from request
+ * @throws Fails when response isn't code 200
+ */
+ queryFn: async ({ newLabel } = {}, { getState, dispatch }) => {
+ try {
+ if (!newLabel) return { data: '' }
+
+ const authUser = getState().auth.user
+ const currentLabels = authUser?.TEMPLATE?.LABELS?.split(',') ?? []
+ const upperCaseLabels = currentLabels.map((l) => l.toUpperCase())
+ const upperCaseNewLabel = newLabel.toUpperCase()
+
+ const exists = upperCaseLabels.some((l) => l === upperCaseNewLabel)
+ if (exists) return { data: upperCaseNewLabel }
+
+ const newLabels = currentLabels.concat(upperCaseNewLabel).join()
+ const template = jsonToXml({ LABELS: newLabels })
+ const queryData = { id: authUser.ID, template, replace: 1 }
+
+ await dispatch(
+ userApi.endpoints.updateUser.initiate(queryData)
+ ).unwrap()
+
+ return { data: upperCaseNewLabel }
+ } catch (error) {
+ return { error }
+ }
+ },
+ }),
+ removeLabel: builder.mutation({
+ /**
+ * @param {object} params - Request parameters
+ * @param {string} params.label - Label to remove
+ * @returns {Promise} Response data from request
+ * @throws Fails when response isn't code 200
+ */
+ queryFn: async ({ label } = {}, { getState, dispatch }) => {
+ try {
+ if (!label) return { data: '' }
+
+ const authUser = getState().auth.user
+ const currentLabels = authUser?.TEMPLATE?.LABELS?.split(',') ?? []
+
+ const newLabels = currentLabels.filter((l) => l !== label).join()
+ const template = jsonToXml({ LABELS: newLabels })
+ const queryData = { id: authUser.ID, template, replace: 1 }
+
+ await dispatch(
+ userApi.endpoints.updateUser.initiate(queryData)
+ ).unwrap()
+
+ return { data: label }
+ } catch (error) {
+ return { error }
+ }
+ },
+ }),
}),
})
@@ -129,6 +191,8 @@ export const {
// Mutations
useLoginMutation,
useChangeAuthGroupMutation,
+ useAddLabelMutation,
+ useRemoveLabelMutation,
} = authApi
export default authApi
diff --git a/src/fireedge/src/client/models/Helper.js b/src/fireedge/src/client/models/Helper.js
index 4072a65003..378a596575 100644
--- a/src/fireedge/src/client/models/Helper.js
+++ b/src/fireedge/src/client/models/Helper.js
@@ -27,7 +27,8 @@ import {
Permission,
UserInputObject,
USER_INPUT_TYPES,
- SERVER_CONFIG,
+ CURRENCY,
+ LOCALE,
} from 'client/constants'
/**
@@ -90,12 +91,9 @@ export const stringToBoolean = (str) =>
*/
export const formatNumberByCurrency = (number, options) => {
try {
- const currency = SERVER_CONFIG?.currency ?? 'EUR'
- const locale = SERVER_CONFIG?.lang?.replace('_', '-') ?? undefined
-
- return Intl.NumberFormat(locale, {
+ return Intl.NumberFormat(LOCALE, {
style: 'currency',
- currency,
+ currency: CURRENCY,
currencyDisplay: 'narrowSymbol',
notation: 'compact',
compactDisplay: 'long',
@@ -107,6 +105,28 @@ export const formatNumberByCurrency = (number, options) => {
}
}
+/**
+ * Function to compare two values.
+ *
+ * @param {Intl.CollatorOptions} options - Options to compare the values
+ * @returns {function(string, string)} - Function to compare two strings
+ * Negative when the referenceStr occurs before compareString
+ * Positive when the referenceStr occurs after compareString
+ * Returns 0 if they are equivalent
+ */
+export const areStringEqual = (options) => (a, b) => {
+ try {
+ const collator = new Intl.Collator(LOCALE, {
+ sensitivity: 'base',
+ ...options,
+ })
+
+ return collator.compare(a, b)
+ } catch {
+ return -1
+ }
+}
+
/**
* Returns `true` if the given value is an instance of Date.
*