diff --git a/ui/assets/icons/OrgOutlinedIcon.js b/ui/assets/icons/OrgOutlinedIcon.js new file mode 100644 index 00000000000..82839cb4711 --- /dev/null +++ b/ui/assets/icons/OrgOutlinedIcon.js @@ -0,0 +1,23 @@ +import React from 'react'; + +function OrgOutlinedIcon({ width = '24px', height = '24px', ...props }) { + return ( + + + + + + + + + + ); +} + +export default OrgOutlinedIcon; diff --git a/ui/assets/icons/WorkspaceOutlined.js b/ui/assets/icons/WorkspaceOutlined.js new file mode 100644 index 00000000000..eec99645871 --- /dev/null +++ b/ui/assets/icons/WorkspaceOutlined.js @@ -0,0 +1,18 @@ +import React from 'react'; + +function WorkspaceOutlinedIcon({ width = '24px', height = '24px', fill = '#F6F8F8' }) { + return ( + + + + + ); +} + +export default WorkspaceOutlinedIcon; diff --git a/ui/components/Header.js b/ui/components/Header.js index e76dde0735a..3f40eeff6f7 100644 --- a/ui/components/Header.js +++ b/ui/components/Header.js @@ -6,7 +6,6 @@ import Hidden from '@material-ui/core/Hidden'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; import { withStyles } from '@material-ui/core/styles'; import { connect, useSelector } from 'react-redux'; import NoSsr from '@material-ui/core/NoSsr'; @@ -45,6 +44,7 @@ import { CustomTextTooltip } from './MesheryMeshInterface/PatternService/CustomT import { Colors } from '@/themes/app'; import CAN from '@/utils/can'; import { keys } from '@/utils/permission_constants'; +import SpaceSwitcher from './SpacesSwitcher/SpaceSwitcher'; const lightColor = 'rgba(255, 255, 255, 0.7)'; const styles = (theme) => ({ @@ -72,12 +72,6 @@ const styles = (theme) => ({ flexGrow: 1, marginRight: 'auto', }, - betaBadge: { color: '#EEEEEE', fontWeight: '300', fontSize: '13px' }, - pageTitle: { - paddingLeft: theme.spacing(2), - fontSize: '1.25rem', - [theme.breakpoints.up('sm')]: { fontSize: '1.65rem' }, - }, appBarOnDrawerOpen: { backgroundColor: theme.palette.secondary.mainBackground, shadowColor: ' #808080', @@ -461,6 +455,7 @@ function K8sContextMenu({ {contexts?.contexts?.map((ctx) => { return ( - - {title} - {isBeta ? BETA : ''} - + ; } @@ -121,6 +121,7 @@ function NavigatorExtension({ StructuredDataFormatter: FormatStructuredData, hooks: { useFilterK8sContexts, + useDynamicComponent, }, }} /> diff --git a/ui/components/SpacesSwitcher/SpaceSwitcher.js b/ui/components/SpacesSwitcher/SpaceSwitcher.js new file mode 100644 index 00000000000..f169aff8b5e --- /dev/null +++ b/ui/components/SpacesSwitcher/SpaceSwitcher.js @@ -0,0 +1,351 @@ +import { useGetOrgsQuery } from '@/rtk-query/organization'; +import { useNotification } from '@/utils/hooks/useNotification'; +import { EVENT_TYPES } from '../../lib/event-types'; +import React, { useEffect, useState } from 'react'; +import { + Button, + FormControl, + FormControlLabel, + FormGroup, + Grid, + MenuItem, + NoSsr, + styled, + TextField, + Typography, + withStyles, + Select, +} from '@material-ui/core'; +import { setKeys, setOrganization, setWorkspace } from '../../lib/store'; +import { connect, Provider } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import styles from './../UserPreferences/style'; +import { store } from '../../store'; +import { withRouter } from 'next/router'; +import OrgOutlinedIcon from '@/assets/icons/OrgOutlinedIcon'; +import { iconXLarge } from 'css/icons.styles'; +import { useGetWorkspacesQuery } from '@/rtk-query/workspace'; +import { useGetCurrentAbilities } from '@/rtk-query/ability'; +import theme from '@/themes/app'; +// import WorkspaceOutlinedIcon from '@/assets/icons/WorkspaceOutlined'; +import { useDynamicComponent } from '@/utils/context/dynamicContext'; +import { UsesSistent } from '../SistentWrapper'; +import _ from 'lodash'; +export const SlideInMenu = styled('div')(() => ({ + width: 0, + overflow: 'hidden', + transition: 'width 2s ease-in' /* Set transition properties */, +})); + +export const SlideInMenuOpen = styled('div')(() => ({ + width: `${(props) => (props.open ? 'auto' : '0')}`, + overflow: 'visible', + transition: ' width 1s ease', +})); + +export const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + textAlign: 'center', + fill: theme.palette.secondary.text, +})); +export const StyledSelect = styled(Select)(() => ({ + paddingTop: '0.5rem', + backgroundColor: 'transparent', + '& .OrgClass': { + display: 'none', + }, + '& svg': { + fill: '#eee', + }, +})); + +export const StyledTextField = styled(TextField)(({ theme }) => ({ + width: '40%', + display: 'flex', + marginBottom: '1.125rem', // 18px converted to rem + marginRight: '0.625rem', // 10px converted to rem + marginTop: '0.5rem', // 8px converted to rem + '& .MuiInput-underline:after': { + borderBottomColor: theme.palette.type === 'dark' ? '#00B39F' : theme.palette.primary, // change the color here + }, + '& .MuiInput': { + fontFamily: 'Qanelas Soft, sans-serif', + }, +})); + +export const StyledHeader = styled(Typography)(({ theme }) => ({ + paddingLeft: theme.spacing(2), + fontSize: '1.25rem', + [theme.breakpoints.up('sm')]: { fontSize: '1.65rem' }, +})); +export const StyledBetaHeader = styled('sup')(() => ({ + color: '#EEEEEE', + fontWeight: '300', + fontSize: '0.8125rem', +})); + +function OrgMenu(props) { + const { + data: orgsResponse, + isSuccess: isOrgsSuccess, + isError: isOrgsError, + error: orgsError, + } = useGetOrgsQuery({}); + let orgs = orgsResponse?.organizations || []; + let uniqueOrgs = _.uniqBy(orgs, 'id'); + const { organization, setOrganization, open } = props; + const [skip, setSkip] = React.useState(true); + const { notify } = useNotification(); + useGetCurrentAbilities(organization, props.setKeys, skip); + useEffect(() => { + if (isOrgsError) { + notify({ + message: `There was an error fetching available data ${orgsError?.data}`, + event_type: EVENT_TYPES.ERROR, + }); + } + }, [isOrgsError, notify, orgsError]); + + const handleOrgSelect = (e) => { + const id = e.target.value; + const selected = orgs.find((org) => org.id === id); + setOrganization({ organization: selected }); + setSkip(false); + }; + return ( + + {isOrgsSuccess && orgs && open && ( + + + + + + + {uniqueOrgs?.map((org) => ( + + + {org.name} + + ))} + + + + } + /> + + + + )} + + ); +} + +export function WorkspaceSwitcher({ organization, open, workspace, setWorkspace }) { + const [orgId, setOrgId] = useState(''); + const { data: workspacesData, isError: isWorkspacesError } = useGetWorkspacesQuery( + { + page: 0, + pagesize: 10, + search: '', + order: '', + orgId: orgId, + }, + { + skip: !orgId ? true : false, + }, + ); + + const handleWorkspaceSelect = (e) => { + const id = e.target.value; + const selected = workspacesData.workspaces.find((org) => org.id === id); + setWorkspace({ workspace: selected }); + }; + + useEffect(() => { + setOrgId(organization?.id); + }, [organization]); + + if (!organization || !workspace) { + return null; + } + + return ( + + {!isWorkspacesError && workspace && ( +
+ + + + + + {workspacesData?.workspaces?.map((works) => ( + + {works.name} + + ))} + + + + } + /> + + +
+ )} +
+ ); +} + +export const FileNameInput = ({ + fileName, + handleFileNameChange, + handleFocus, + activateWalkthrough, +}) => { + return ( + activateWalkthrough && activateWalkthrough()} + /> + ); +}; + +function DefaultHeader({ title, isBeta }) { + return ( + + {title} + {isBeta ? BETA : ''} + + ); +} + +function SpaceSwitcher(props) { + const [orgOpen, setOrgOpen] = useState(false); + // const [workspaceOpen, setWorkspaceOpen] = useState(false); + const { DynamicComponent } = useDynamicComponent(); + return ( + + + +
+ + / + {/* / + + / */} +
+ {!DynamicComponent && } +
+ + + + ); +} + +const mapStateToProps = (state) => { + const organization = state.get('organization'); + const workspace = state.get('workspace'); + return { + organization, + workspace, + }; +}; + +const mapDispatchToProps = (dispatch) => ({ + setOrganization: bindActionCreators(setOrganization, dispatch), + setWorkspace: bindActionCreators(setWorkspace, dispatch), + setKeys: bindActionCreators(setKeys, dispatch), +}); + +export default withStyles(styles)( + connect(mapStateToProps, mapDispatchToProps)(withRouter(SpaceSwitcher)), +); diff --git a/ui/components/UserPreferences/index.js b/ui/components/UserPreferences/index.js index 405f5ac3f49..877bf36c047 100644 --- a/ui/components/UserPreferences/index.js +++ b/ui/components/UserPreferences/index.js @@ -40,7 +40,6 @@ import { getTheme, setTheme } from '../../utils/theme'; import { isExtensionOpen } from '../../pages/_app'; import { EVENT_TYPES } from '../../lib/event-types'; import { useNotification } from '../../utils/hooks/useNotification'; -import SpacesPreferences from './spaces-preferences'; import { CustomTextTooltip } from '../MesheryMeshInterface/PatternService/CustomTextTooltip'; import { CHARCOAL } from '@layer5/sistent'; import { useWindowDimensions } from '@/utils/dimension'; @@ -693,7 +692,6 @@ const UserPreference = (props) => { {tabVal === 0 && ( <> -
diff --git a/ui/lib/store.js b/ui/lib/store.js index 7bcb66b406b..32dccf9417c 100644 --- a/ui/lib/store.js +++ b/ui/lib/store.js @@ -73,11 +73,13 @@ const initialState = fromJS({ meshSyncState: null, connectionMetadataState: null, // store connection definition metadata for state and connection kind management organization: null, + workspace: null, keys: null, }); export const actionTypes = { UPDATE_PAGE: 'UPDATE_PAGE', + SET_WORKSPACE: 'SET_WORKSPACE', UPDATE_TITLE: 'UPDATE_TITLE', UPDATE_USER: 'UPDATE_USER', UPDATE_BETA_BADGE: 'UPDATE_BETA_BADGE', @@ -212,7 +214,10 @@ export const reducer = (state = initialState, action) => { const updatedOrgState = state.mergeDeep({ organization: action.organization }); sessionStorage.setItem('currentOrg', JSON.stringify(action.organization)); return updatedOrgState; - + case actionTypes.SET_WORKSPACE: + const updatedWorkspaceState = state.mergeDeep({ workspace: action.workspace }); + sessionStorage.setItem('currentWorkspace', JSON.stringify(action.workspace)); + return updatedWorkspaceState; default: return state; } @@ -387,7 +392,11 @@ export const setOrganization = (dispatch) => { return dispatch({ type: actionTypes.SET_ORGANIZATION, organization }); }; - +export const setWorkspace = + ({ workspace }) => + (dispatch) => { + return dispatch({ type: actionTypes.SET_WORKSPACE, workspace }); + }; export const setKeys = ({ keys }) => (dispatch) => { diff --git a/ui/pages/_app.js b/ui/pages/_app.js index c2618cc666b..932a47cd70a 100644 --- a/ui/pages/_app.js +++ b/ui/pages/_app.js @@ -63,6 +63,7 @@ import { getMeshModelComponentByName } from '../api/meshmodel'; import { CONNECTION_KINDS, CONNECTION_KINDS_DEF, CONNECTION_STATES } from '../utils/Enum'; import { ability } from '../utils/can'; import { getCredentialByID } from '@/api/credentials'; +import { DynamicComponentProvider } from '@/utils/context/dynamicContext'; if (typeof window !== 'undefined') { require('codemirror/mode/yaml/yaml'); @@ -276,7 +277,7 @@ class MesheryApp extends App { const res = await getMeshModelComponentByName(formatToTitleCase(kind).concat('Connection')); if (res?.components) { connectionDef[CONNECTION_KINDS[kind]] = { - transitions: res?.components[0].metadata.transitions, + transitions: res?.components[0].model.metadata.transitions, icon: res?.components[0].metadata.svgColor, }; } @@ -411,6 +412,7 @@ class MesheryApp extends App { let org = JSON.parse(currentOrg); await this.loadAbility(org.id, reFetchKeys); this.setOrganization(org); + await this.loadWorkspace(org.id); } dataFetch( @@ -429,11 +431,13 @@ class MesheryApp extends App { organizationToSet = result.organizations[0]; reFetchKeys = true; await this.loadAbility(organizationToSet.id, reFetchKeys); + await this.loadWorkspace(organizationToSet.id); this.setOrganization(organizationToSet); } } else { organizationToSet = result.organizations[0]; reFetchKeys = true; + await this.loadWorkspace(organizationToSet.id); await this.loadAbility(organizationToSet.id, reFetchKeys); this.setOrganization(organizationToSet); } @@ -441,7 +445,25 @@ class MesheryApp extends App { (err) => console.log('There was an error fetching available orgs:', err), ); }; - + loadWorkspace = async (orgId) => { + const currentWorkspace = sessionStorage.getItem('currentWorkspace'); + if (currentWorkspace && currentWorkspace !== 'undefined') { + let workspace = JSON.parse(currentWorkspace); + this.setWorkspace(workspace); + } else { + dataFetch( + `/api/workspaces?search=&order=&page=0&pagesize=10&orgID=${orgId}`, + { + method: 'GET', + credentials: 'include', + }, + async (result) => { + this.setWorkspace(result.workspaces[0]); + }, + (err) => console.log('There was an error fetching workspaces:', err), + ); + } + }; setOrganization = (org) => { const { store } = this.props; store.dispatch({ @@ -449,7 +471,13 @@ class MesheryApp extends App { organization: org, }); }; - + setWorkspace = (workspace) => { + const { store } = this.props; + store.dispatch({ + type: actionTypes.SET_WORKSPACE, + workspace: workspace, + }); + }; loadAbility = async (orgID, reFetchKeys) => { const storedKeys = sessionStorage.getItem('keys'); const { store } = this.props; @@ -575,149 +603,151 @@ class MesheryApp extends App { const { Component, pageProps, classes, isDrawerCollapsed, relayEnvironment } = this.props; return ( - - - - -
- - {!this.state.isFullScreenMode && ( - - )} -
- , - error: , - warning: , - info: , - }} - classes={{ - variantSuccess: - this.state.theme === 'dark' - ? classes.darknotifSuccess - : classes.notifSuccess, - variantError: - this.state.theme === 'dark' ? classes.darknotifError : classes.notifError, - variantWarning: - this.state.theme === 'dark' ? classes.darknotifWarn : classes.notifWarn, - variantInfo: - this.state.theme === 'dark' ? classes.darknotifInfo : classes.notifInfo, - }} - maxSnack={10} - > - - - {!this.state.isFullScreenMode && ( -
+ + + + +
+ + {!this.state.isFullScreenMode && ( + + )} +
+ , + error: , + warning: , + info: , + }} + classes={{ + variantSuccess: + this.state.theme === 'dark' + ? classes.darknotifSuccess + : classes.notifSuccess, + variantError: + this.state.theme === 'dark' ? classes.darknotifError : classes.notifError, + variantWarning: + this.state.theme === 'dark' ? classes.darknotifWarn : classes.notifWarn, + variantInfo: + this.state.theme === 'dark' ? classes.darknotifInfo : classes.notifInfo, + }} + maxSnack={10} + > + + + {!this.state.isFullScreenMode && ( +
- - - - -
- +
+ + + + + +
+ +
- - {this.props.capabilitiesRegistry?.restrictedAccess - ?.isMesheryUiRestricted ? ( - 'ACCESS LIMITED IN MESHERY PLAYGROUND. DEPLOY MESHERY TO ACCESS ALL FEATURES.' - ) : ( - <> - {' '} - Built with{' '} - {' '} - by the Layer5 Community - - )} - - -
+ + + {this.props.capabilitiesRegistry?.restrictedAccess + ?.isMesheryUiRestricted ? ( + 'ACCESS LIMITED IN MESHERY PLAYGROUND. DEPLOY MESHERY TO ACCESS ALL FEATURES.' + ) : ( + <> + {' '} + Built with{' '} + {' '} + by the Layer5 Community + + )} + + +
+
-
- this.setState({ isOpen: false })} - isOpen={this.state.isOpen} - /> - - - - + this.setState({ isOpen: false })} + isOpen={this.state.isOpen} + /> + + + + + ); } } diff --git a/ui/pages/_document.js b/ui/pages/_document.js index 46dba20b6ff..c80e54d50e1 100644 --- a/ui/pages/_document.js +++ b/ui/pages/_document.js @@ -117,7 +117,6 @@ MesheryDocument.getInitialProps = (ctx) => { if (pageContext) { css = pageContext.sheetsRegistry.toString(); } - return { ...page, pageContext, diff --git a/ui/rtk-query/ability.js b/ui/rtk-query/ability.js index fd2fa98c251..2fd43d36a47 100644 --- a/ui/rtk-query/ability.js +++ b/ui/rtk-query/ability.js @@ -11,7 +11,7 @@ export const useGetUserAbilities = (org, skip) => { const [getUserQuery] = useLazyGetUserKeysQuery(); useEffect(() => { - getUserQuery({ orgId: org.id }, { skip }) + getUserQuery({ orgId: org?.id }, { skip }) .unwrap() .then((res) => { const abilities = res.keys?.map((key) => ({ @@ -27,7 +27,7 @@ export const useGetUserAbilities = (org, skip) => { .catch((error) => { console.error('Error when fetching keys in useGetUserAbilities custom hook', error); }); - }, [org.id, getUserQuery, skip]); + }, [org?.id, getUserQuery, skip]); return data; }; diff --git a/ui/themes/app.js b/ui/themes/app.js index 78fabbb0134..2e79735cba0 100644 --- a/ui/themes/app.js +++ b/ui/themes/app.js @@ -280,7 +280,7 @@ darkTheme = { MuiCssBaseline: { '@global': { body: { - backgroundColor: '#303030', + backgroundColor: '#303030 !important', // scrollbarColor : "#6b6b6b #263238", '&::-webkit-scrollbar, & *::-webkit-scrollbar': { backgroundColor: '#1A1A1A', @@ -536,7 +536,7 @@ theme = { MuiCssBaseline: { '@global': { body: { - backgroundColor: '#e9eff1', + backgroundColor: '#e9eff1 !important', // scrollbarColor : "#6b6b6b #263238", '&::-webkit-scrollbar, & *::-webkit-scrollbar': { backgroundColor: '#d7d7d7', diff --git a/ui/utils/context/dynamicContext.js b/ui/utils/context/dynamicContext.js new file mode 100644 index 00000000000..d37d41d0a55 --- /dev/null +++ b/ui/utils/context/dynamicContext.js @@ -0,0 +1,23 @@ +import React, { useState } from 'react'; + +// Create a context to hold the dynamic component +const DynamicComponentContext = React.createContext(); + +// Custom hook to use the dynamic component +export const useDynamicComponent = () => { + const context = React.useContext(DynamicComponentContext); + if (!context) { + throw new Error('useDynamicComponent must be used within a DynamicComponentProvider'); + } + return context; +}; + +// Provider component to set the dynamic component +export const DynamicComponentProvider = ({ children }) => { + const [DynamicComponent, setComponent] = useState(null); + return ( + + {children} + + ); +};