From 63c7de92ad99e6da39ebc585b51f8d558ab5dc3f Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 01:57:45 +0100 Subject: [PATCH 01/49] Nav poc --- frontend/common/constants.ts | 1 + .../common/providers/OrganisationProvider.tsx | 4 +- frontend/common/providers/Permission.tsx | 2 +- frontend/common/stores/account-store.js | 2 +- frontend/common/utils/utils.tsx | 11 +- frontend/web/components/App.js | 412 ++-- .../web/components/BreadcrumbSeparator.tsx | 9 + frontend/web/components/ButterBar.tsx | 3 +- frontend/web/components/Collapsible.js | 31 +- frontend/web/components/ErrorMessage.js | 3 +- frontend/web/components/GithubStar.tsx | 2 +- frontend/web/components/NavSubLink.tsx | 26 + frontend/web/components/OrganisationUsage.tsx | 10 +- .../web/components/ProjectManageWidget.tsx | 22 +- frontend/web/components/ProjectsPage.tsx | 36 + frontend/web/components/WarningMessage.tsx | 3 +- .../web/components/modals/CreateProject.js | 3 +- frontend/web/components/modals/InviteUsers.js | 3 +- frontend/web/components/modals/Payment.js | 2 +- .../components/pages/AccountSettingsPage.js | 3 +- .../web/components/pages/AuditLogPage.tsx | 11 +- .../components/pages/ChangeRequestsPage.js | 3 +- .../web/components/pages/ConfirmEmailPage.js | 2 +- .../pages/CreateOrganisationPage.js | 2 +- .../pages/EnvironmentSettingsPage.js | 4 +- .../components/pages/FeatureHistoryPage.tsx | 2 - frontend/web/components/pages/HomeAside.tsx | 244 +++ frontend/web/components/pages/InvitePage.js | 2 +- .../pages/OrganisationSettingsPage.js | 890 +------- .../OrganisationSettingsRedirectPage.tsx | 27 + .../pages/OrganisationUsagePage.tsx | 24 + .../pages/ProjectPermissionsPage.tsx | 9 + .../components/pages/ProjectSettingsPage.js | 2 +- .../components/pages/ScheduledChangesPage.js | 3 +- .../web/components/pages/SegmentsPage.tsx | 3 +- frontend/web/components/pages/UserPage.js | 1800 +++++++++-------- .../pages/UsersAndPermissionsPage.tsx | 739 +++++++ frontend/web/components/svg/SettingsIcon.js | 25 + frontend/web/routes.js | 30 +- frontend/web/styles/3rdParty/_hljs.scss | 2 +- frontend/web/styles/components/_aside.scss | 547 ----- frontend/web/styles/new/_variables-new.scss | 27 +- frontend/web/styles/project/_alert.scss | 8 - frontend/web/styles/project/_icons.scss | 2 +- frontend/web/styles/project/_layout.scss | 10 +- frontend/web/styles/project/_popover.scss | 4 - frontend/web/styles/project/_project-nav.scss | 233 +-- frontend/web/styles/styles.scss | 3 +- 48 files changed, 2553 insertions(+), 2693 deletions(-) create mode 100644 frontend/web/components/BreadcrumbSeparator.tsx create mode 100644 frontend/web/components/NavSubLink.tsx create mode 100644 frontend/web/components/ProjectsPage.tsx create mode 100644 frontend/web/components/pages/HomeAside.tsx create mode 100644 frontend/web/components/pages/OrganisationSettingsRedirectPage.tsx create mode 100644 frontend/web/components/pages/OrganisationUsagePage.tsx create mode 100644 frontend/web/components/pages/ProjectPermissionsPage.tsx create mode 100644 frontend/web/components/pages/UsersAndPermissionsPage.tsx create mode 100644 frontend/web/components/svg/SettingsIcon.js diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts index 2d8830ce88a3..3876a5a013b8 100644 --- a/frontend/common/constants.ts +++ b/frontend/common/constants.ts @@ -522,4 +522,5 @@ export default { '#DE3163', ], untaggedTag: { color: '#dedede', label: 'Untagged' }, + upgradeURL: '/organisation-settings?tab=billing', } diff --git a/frontend/common/providers/OrganisationProvider.tsx b/frontend/common/providers/OrganisationProvider.tsx index 97c548e7720f..13f0123f4517 100644 --- a/frontend/common/providers/OrganisationProvider.tsx +++ b/frontend/common/providers/OrganisationProvider.tsx @@ -15,12 +15,13 @@ import { useGetGroupsQuery } from 'common/services/useGroup' type OrganisationProviderType = { onRemoveProject?: () => void onSave?: (data: { environmentId: number; projectId: number }) => void - id?: number + id?: number | string children: (props: { createProject: typeof AppActions.createProject invalidateInviteLink: typeof AppActions.invalidateInviteLink inviteLinks: InviteLink[] | null invites: Invite[] | null + error: any isLoading: boolean isSaving: boolean name: string @@ -77,6 +78,7 @@ const OrganisationProvider: FC = ({ {children({ createProject: AppActions.createProject, groups: groups?.results || [], + error: AccountStore.error, invalidateInviteLink: AppActions.invalidateInviteLink, inviteLinks: OrganisationStore.getInviteLinks(), invites: OrganisationStore.getInvites(), diff --git a/frontend/common/providers/Permission.tsx b/frontend/common/providers/Permission.tsx index 1852b7dc940b..0be20f8697fb 100644 --- a/frontend/common/providers/Permission.tsx +++ b/frontend/common/providers/Permission.tsx @@ -39,7 +39,7 @@ const Permission: FC = ({ {children({ isLoading, permission: hasPermission || AccountStore.isAdmin(), - }) ||
} + }) || null} ) } diff --git a/frontend/common/stores/account-store.js b/frontend/common/stores/account-store.js index 8a05dd10f7fb..bfab6715ed17 100644 --- a/frontend/common/stores/account-store.js +++ b/frontend/common/stores/account-store.js @@ -52,7 +52,7 @@ const controller = { store.saved() if (isLoginPage) { - window.location.href = `/organisation-settings` + window.location.href = `/organisations` } }) .catch((e) => { diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 4eeae5bb26d4..75dd00fba8dd 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -384,6 +384,14 @@ const Utils = Object.assign({}, require('./base/_utils'), { return Constants.projectColors[index % (Constants.projectColors.length - 1)] }, + getOrganisationHomePage(id?: string) { + const orgId = id || AccountStore.getOrganisation()?.id + if (!orgId) { + return `/organisations` + } + return `/organisation/${orgId}/projects` + }, + getSDKEndpoint(_project: ProjectType) { const project = _project || ProjectStore.model @@ -486,7 +494,6 @@ const Utils = Object.assign({}, require('./base/_utils'), { id ? `${id}/` : '' }` }, - getViewIdentitiesPermission() { return 'VIEW_IDENTITIES' }, @@ -518,6 +525,7 @@ const Utils = Object.assign({}, require('./base/_utils'), { head.appendChild(script) }) }, + numberWithCommas(x: number) { if (typeof x !== 'number') return '' return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') @@ -535,7 +543,6 @@ const Utils = Object.assign({}, require('./base/_utils'), { zE('messenger', 'open') } }, - removeElementFromArray(array: any[], index: number) { return array.slice(0, index).concat(array.slice(index + 1)) }, diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index 423830e29043..7bc13f046ed5 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -1,9 +1,8 @@ import React, { Component, Fragment } from 'react' import { matchPath } from 'react-router' -import { withRouter } from 'react-router-dom' +import { Link, withRouter } from 'react-router-dom' import amplitude from 'amplitude-js' import NavLink from 'react-router-dom/NavLink' -import Aside from './Aside' import TwoFactorPrompt from './SimpleTwoFactor/prompt' import Maintenance from './Maintenance' import Blocked from './Blocked' @@ -25,6 +24,17 @@ import InfoMessage from './InfoMessage' import OrganisationLimit from './OrganisationLimit' import GithubStar from './GithubStar' import Tooltip from './Tooltip' +import classNames from 'classnames' +import { apps, gitCompare, home, statsChart } from 'ionicons/icons' +import NavSubLink from './NavSubLink' +import SettingsIcon from './svg/SettingsIcon' +import UsersIcon from './svg/UsersIcon' +import BreadcrumbSeparator from './BreadcrumbSeparator' +import OrganisationStore from 'common/stores/organisation-store' +import SegmentsIcon from './svg/SegmentsIcon' +import AuditLogIcon from './svg/AuditLogIcon' +import Permission from 'common/providers/Permission' +import HomeAside from './pages/HomeAside' const App = class extends Component { static propTypes = { @@ -49,8 +59,44 @@ const App = class extends Component { ES6Component(this) } + getProjectId = (props) => { + const { location } = props + const pathname = location.pathname + + const match = matchPath(pathname, { + exact: false, + path: '/project/:projectId/environment/:environmentId', + strict: false, + }) + const match2 = matchPath(pathname, { + exact: false, + path: '/project/:projectId', + strict: false, + }) + const projectId = + _.get(match, 'params.projectId') || _.get(match2, 'params.projectId') + return projectId + } + getEnvironmentId = (props) => { + const { location } = props + const pathname = location.pathname + + const match = matchPath(pathname, { + exact: false, + path: '/project/:projectId/environment/:environmentId', + strict: false, + }) + + const environmentId = _.get(match, 'params.environmentId') + return environmentId + } + componentDidMount = () => { getBuildVersion() + this.state.projectId = this.getProjectId(this.props) + if (this.state.projectId) { + AppActions.getProject(this.state.projectId) + } this.listenTo(ProjectStore, 'change', () => this.forceUpdate()) this.listenTo(AccountStore, 'change', this.getOrganisationUsage) this.getOrganisationUsage() @@ -93,6 +139,12 @@ const App = class extends Component { componentDidUpdate(prevProps) { if (prevProps.location.pathname !== this.props.location.pathname) { + const newProjectId = this.getProjectId(this.props) + if (this.state.projectId !== newProjectId && !!newProjectId) { + this.state.projectId = newProjectId + AppActions.getProject(this.state.projectId) + } + if (isMobile) { this.setState({ asideIsVisible: false }) } @@ -176,7 +228,7 @@ const App = class extends Component { id: lastEnv.orgId, }) if (!lastOrg) { - this.context.router.history.replace('/organisation-settings') + this.context.router.history.replace('/select-organistion') return } @@ -192,7 +244,7 @@ const App = class extends Component { return } - this.context.router.history.replace('/organisation-settings') + this.context.router.history.replace(Utils.getOrganisationHomePage()) }) } } @@ -236,19 +288,8 @@ const App = class extends Component { const { location } = this.props const pathname = location.pathname const { asideIsVisible, lastEnvironmentId, lastProjectId } = this.state - const match = matchPath(pathname, { - exact: false, - path: '/project/:projectId/environment/:environmentId', - strict: false, - }) - const match2 = matchPath(pathname, { - exact: false, - path: '/project/:projectId', - strict: false, - }) - const projectId = - _.get(match, 'params.projectId') || _.get(match2, 'params.projectId') - const environmentId = _.get(match, 'params.environmentId') + const projectId = this.getProjectId(this.props) + const environmentId = this.getEnvironmentId(this.props) const storageHasParams = lastEnvironmentId || lastProjectId const pageHasAside = environmentId || projectId || storageHasParams @@ -301,6 +342,11 @@ const App = class extends Component { Utils.getFlagsmithHasFeature('announcement') && this.state.showAnnouncement + const isOrgSelect = document.location.pathname === '/organisations' + const activeProject = OrganisationStore.getProject(projectId) + const integrations = Object.keys( + JSON.parse(Utils.getFlagsmithValue('integration_data') || '{}'), + ) return ( - {({ isSaving, user }, { twoFactorLogin }) => - user && user.twoFactorPrompt ? ( + {({ isSaving, user }, { twoFactorLogin }) => { + const inner = ( +
+ + {projectNotLoaded ? ( +
+ +
+ ) : ( + + {user && ( + + )} + {user && showBanner && ( + + + this.closeAnnouncement(announcementValue.id) + } + buttonText={announcementValue.buttonText} + url={announcementValue.url} + > +
+
{announcementValue.description}
+
+
+
+ )} + {this.props.children} +
+ )} +
+ ) + return user && user.twoFactorPrompt ? (
) : ( -
-
+
+
{!isHomepage && (!pageHasAside || !asideIsVisible || !isMobile) && ( -
) - } + }} ) diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx new file mode 100644 index 000000000000..0bb7d2325f07 --- /dev/null +++ b/frontend/web/components/BreadcrumbSeparator.tsx @@ -0,0 +1,9 @@ +import { FC } from 'react' + +type BreadcrumbSeparatorType = {} + +const BreadcrumbSeparator: FC = ({}) => { + return / +} + +export default BreadcrumbSeparator diff --git a/frontend/web/components/ButterBar.tsx b/frontend/web/components/ButterBar.tsx index 3d2987c91ee3..b7975d9e8954 100644 --- a/frontend/web/components/ButterBar.tsx +++ b/frontend/web/components/ButterBar.tsx @@ -8,6 +8,7 @@ import Utils from 'common/utils/utils' import { Environment, FeatureImport, Res } from 'common/types/responses' import { useGetFeatureImportsQuery } from 'common/services/useFeatureImport' import AppActions from 'common/dispatcher/app-actions' +import Constants from 'common/constants'; interface ButterBarProps { billingStatus?: string @@ -92,7 +93,7 @@ const ButterBar: React.FC = ({ billingStatus, projectId }) => { {Utils.getFlagsmithHasFeature('read_only_mode') && (
Your organisation is over its usage limit, please{' '} - upgrade your plan. + upgrade your plan.
)} {Utils.getFlagsmithHasFeature('show_dunning_banner') && diff --git a/frontend/web/components/Collapsible.js b/frontend/web/components/Collapsible.js index 6ea77bb90f26..f49eb7673588 100644 --- a/frontend/web/components/Collapsible.js +++ b/frontend/web/components/Collapsible.js @@ -1,5 +1,7 @@ -import { PureComponent } from 'react' +import React, { PureComponent } from 'react' import Icon from './Icon' +import { IonIcon } from '@ionic/react' +import { chevronDown, chevronForward, createOutline } from 'ionicons/icons' const cn = require('classnames') @@ -36,22 +38,19 @@ const Collapsible = class extends PureComponent { })} ref={this.ref} > -
- +
+
+ +
{this.props.title}
-
- -
- +
{this.props.active ? (
{this.props.children}
diff --git a/frontend/web/components/ErrorMessage.js b/frontend/web/components/ErrorMessage.js index 5e7e0fd87050..cd3dd7d5454c 100644 --- a/frontend/web/components/ErrorMessage.js +++ b/frontend/web/components/ErrorMessage.js @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react' import Icon from './Icon' import Button from './base/forms/Button' import Format from 'common/utils/format' +import Constants from 'common/constants'; export default class ErrorMessage extends PureComponent { static displayName = 'ErrorMessage' @@ -40,7 +41,7 @@ export default class ErrorMessage extends PureComponent { -

-
-
-
-
- {isLoading && ( -
- -
- )} - {!isLoading && ( -
- - - -
- Team Members -
- {Utils.renderWithPermission( - !this.state.permissionsError, - Constants.organisationPermissions( - 'Admin', - ), - , - )} -
- - {paymentsEnabled && - !isLoading && ( -
- - { - 'You are currently using ' - } - - {`${organisation.num_seats} of ${max_seats}`} - - {` seat${ - organisation.num_seats === - 1 - ? '' - : 's' - } `}{' '} - for your plan.{' '} - {usedSeats && ( - <> - {overSeats && - (!verifySeatsLimit || - !autoSeats) ? ( - - If you wish to - invite any - additional - members, please{' '} - { - - Contact us - - } - . - - ) : needsUpgradeForAdditionalSeats ? ( - - If you wish to - invite any - additional - members, please{' '} - { - { - this.props.router.history.replace( - `${ - document - .location - .pathname - }?${Utils.toParam( - { - ...Utils.fromParam(), - tab: this - .state - .tab - ? 'closed' - : 'open', - }, - )}`, - ) - }} - > - Upgrade your - plan - - } - . - - ) : ( - - You will - automatically be - charged $20/month - for each - additional member - that joins your - organisation. - - )} - - )} - -
- )} - {inviteLinks && - (!verifySeatsLimit || - !needsUpgradeForAdditionalSeats) && ( -
{ - e.preventDefault() - }} - > -
- -
- - f.role === - this.state - .role, - ).hash - }`} - data-test='invite-link' - inputClassName='input input--wide' - type='text' - readonly='readonly' - title={ -

Link

- } - placeholder='Link' - size='small' - /> - - - - - - - )} - -
-

- Anyone with link can join - as a standard user, once - they have joined you can - edit their role from the - team members panel.{' '} - -

-
- {error && ( - - )} -
- - )} - - - User - -
- Role -
-
- Last logged in -
-
- Actions -
-
- } - items={users} - itemHeight={65} - renderRow={(user, i) => { - const { - email, - first_name, - id, - last_login, - last_name, - role, - } = user - - const onRemoveClick = () => { - this.deleteUser( - id, - Format.userDisplayName({ - email, - firstName: first_name, - lastName: last_name, - }), - email, - ) - } - const onEditClick = () => { - if (role !== 'ADMIN') { - this.editUserPermissions( - user, - organisation.id, - ) - } - } - return ( - - - {`${first_name} ${last_name}`}{' '} - {id === - AccountStore.getUserId() && - '(You)'} -
- {email} -
-
- -
-
- {organisation.role === - 'ADMIN' && - id !== - AccountStore.getUserId() ? ( -
- + this.setState({ + role: v.value, + }) + } + options={[ + { + label: 'Organisation Administrator', + value: 'ADMIN', + }, + { + isDisabled: !hasRbacPermission, + label: hasRbacPermission + ? 'User' + : 'User - Please upgrade for role based access', + value: 'USER', + }, + ]} + className='react-select select-sm' + /> +
+ {inviteLinks.find( + (f) => f.role === this.state.role, + ) && ( + <> + + f.role === this.state.role, + ).hash + }`} + data-test='invite-link' + inputClassName='input input--wide' + type='text' + readonly='readonly' + title={

Link

} + placeholder='Link' + size='small' + /> +
+ + + + + + )} + +
+

+ Anyone with link can join as a standard user, + once they have joined you can edit their role + from the team members panel.{' '} + +

+
+ {error && } +
+ + )} + + User +
+ Role +
+
+ Last logged in +
+
+ Actions +
+ + } + items={users} + itemHeight={65} + renderRow={(user, i) => { + const { + email, + first_name, + id, + last_login, + last_name, + role, + } = user + + const onRemoveClick = () => { + this.deleteUser( + id, + Format.userDisplayName({ + email, + firstName: first_name, + lastName: last_name, + }), + email, + ) + } + const onEditClick = () => { + if (role !== 'ADMIN') { + this.editUserPermissions(user, organisation.id) + } + } + return ( + + + {`${first_name} ${last_name}`}{' '} + {id === AccountStore.getUserId() && '(You)'} +
+ {email} +
+
+ +
+
+ {organisation.role === 'ADMIN' && + id !== AccountStore.getUserId() ? ( +
+ + navigateOrganisations(e, user.organisations) + } + search + inputClassName='border-0 border-bottom-1' + size='xSmall' + className='full-width' + placeholder='Search Organisations...' + /> + { + setHoveredOrganisation(organisation.id) + }} + onClick={(organisation: Organisation) => { + AppActions.selectOrganisation(organisation.id) + AppActions.getOrganisation(organisation.id) + router.history.push(Utils.getOrganisationHomePage()) + }} + /> +
+
+ navigateOrganisations(e)} + search + className='full-width' + inputClassName='border-0 border-bottom-1' + size='xSmall' + placeholder='Search Projects...' + /> + { + getEnvironments(getStore(), { + projectId: `${project.id}`, + }).then((res: { data: PagedResponse }) => { + router.history.push( + `/project/${project.id}/environment/${ + res.data?.results?.length && res.data?.results?.[0] + ? `${res.data?.results?.[0].api_key}/features` + : 'create' + }`, + ) + }) + }} + /> +
+
+ ) + }} + + +
+ ) } export default BreadcrumbSeparator diff --git a/frontend/web/components/InlineModal.js b/frontend/web/components/InlineModal.js index b8e395610a2e..3231e2336864 100644 --- a/frontend/web/components/InlineModal.js +++ b/frontend/web/components/InlineModal.js @@ -16,6 +16,7 @@ class InlineModal extends PureComponent { isOpen: propTypes.bool, onBack: propTypes.func, onClose: propTypes.func, + relativeToParent: propTypes.bool, showBack: propTypes.bool, title: propTypes.string, } @@ -29,7 +30,7 @@ class InlineModal extends PureComponent { render() { // const { props } = this; return ( -
+
{this.props.isOpen && (
{(!!this.props.title || !this.props.hideClose) && ( diff --git a/frontend/web/components/OrganisationSelect.js b/frontend/web/components/OrganisationSelect.js index 1e23fbbd1c05..9be683f5aaea 100644 --- a/frontend/web/components/OrganisationSelect.js +++ b/frontend/web/components/OrganisationSelect.js @@ -29,7 +29,6 @@ const OrganisationSelect = class extends Component { value: AccountStore.getOrganisation().id, }} onChange={({ value }) => { - API.setCookie('organisation', `${value}`) this.props.onChange && this.props.onChange(value) }} options={ diff --git a/frontend/web/styles/3rdParty/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss index 03805685988b..724d87291d7c 100644 --- a/frontend/web/styles/3rdParty/_hljs.scss +++ b/frontend/web/styles/3rdParty/_hljs.scss @@ -1,5 +1,7 @@ @import '../mixins/custom-scrollbar'; - +.custom-scroll { + @include customScroll() +} @mixin transition($transition...) { -moz-transition: $transition; -o-transition: $transition; diff --git a/frontend/web/styles/project/_modals.scss b/frontend/web/styles/project/_modals.scss index fa52f036e80b..9281b7ef787b 100644 --- a/frontend/web/styles/project/_modals.scss +++ b/frontend/web/styles/project/_modals.scss @@ -143,7 +143,7 @@ $side-width: 660px; position: absolute; background: $body-bg; border-radius: $border-radius; - z-index: 1; + z-index: 2; box-shadow: rgba(75, 75, 75, 0.11) 0px 1px 6px 0px, rgba(75, 75, 75, 0.11) 0px 1px 4px 0px; border: 1px solid $input-border-color; diff --git a/frontend/web/styles/project/_utils.scss b/frontend/web/styles/project/_utils.scss index eaf0e00c2cc6..8120b323aa19 100644 --- a/frontend/web/styles/project/_utils.scss +++ b/frontend/web/styles/project/_utils.scss @@ -52,7 +52,23 @@ .float-left{ float:left; } - +.left-0 { + left: 0 !important; +} +.max-w-auto { + max-width: none !important; +} +.top-form-item { + top: 30px; +} +.border-left-1 { + border-left: 1px solid $hr-border-color !important; +} +.border-bottom-1 { + border-bottom: 1px solid $hr-border-color !important; + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} .margin-top{ margin-top:20px; } From c2aa3af0b61c0d47cb19945e1105be404aae94f6 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 13:45:40 +0100 Subject: [PATCH 15/49] neaten breadcrumb --- frontend/web/components/BreadcrumbSeparator.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx index 92ce09152271..34fbdb842270 100644 --- a/frontend/web/components/BreadcrumbSeparator.tsx +++ b/frontend/web/components/BreadcrumbSeparator.tsx @@ -78,7 +78,9 @@ const ItemList: FC = ({ )} > {v.name} - {isActive && } + {isActive && ( + + )} ) })} From d4cb892a81f128d6667fb2a8f7dbf1f2eeb79dcf Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 13:57:25 +0100 Subject: [PATCH 16/49] Add org filtering --- frontend/web/components/App.js | 2 + .../web/components/BreadcrumbSeparator.tsx | 165 +++++++++++------- 2 files changed, 103 insertions(+), 64 deletions(-) diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index fbc3580c5df6..e49696175978 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -448,6 +448,7 @@ const App = class extends Component { projectId={projectId} router={this.context.router} hideSlash={!activeProject} + focus='organisation' > = ({ className, isLoading, - items, + items: _items, onClick, onHover, + search, title, value, }) => { + const items = search + ? _items?.filter((v) => { + return v.name.toLowerCase().includes(search.toLowerCase()) + }) + : _items return (
= ({
)} + {items?.length === 0 ? ( + search ? ( +
+ No results found for "{search}" +
+ ) : ( +
No Results
+ ) + ) : null} {items?.map((v) => { const isActive = `${v.id}` === `${value}` return ( @@ -90,12 +107,15 @@ const ItemList: FC = ({ const BreadcrumbSeparator: FC = ({ children, + focus, hideDropdown, hideSlash, projectId, router, }) => { const [open, setOpen] = useState(false) + const [organisationSearch, setOrganisationSearch] = useState('') + const [projectSearch, setProjectSearch] = useState('') const [activeOrganisation, setActiveOrganisation] = useState( AccountStore.getOrganisation()?.id, @@ -177,75 +197,92 @@ const BreadcrumbSeparator: FC = ({ hideClose relativeToParent isOpen={open} - onClose={setOpen} + onClose={() => { + setOpen(false) + setProjectSearch('') + setOrganisationSearch('') + }} containerClassName={'p-0'} className={ 'inline-modal left-0 top-form-item inline-modal--sm max-w-auto' } > - - {({ user }: { user: User }) => { - return ( -
-
- - navigateOrganisations(e, user.organisations) - } - search - inputClassName='border-0 border-bottom-1' - size='xSmall' - className='full-width' - placeholder='Search Organisations...' - /> - { - setHoveredOrganisation(organisation.id) - }} - onClick={(organisation: Organisation) => { - AppActions.selectOrganisation(organisation.id) - AppActions.getOrganisation(organisation.id) - router.history.push(Utils.getOrganisationHomePage()) - }} - /> -
-
- navigateOrganisations(e)} - search - className='full-width' - inputClassName='border-0 border-bottom-1' - size='xSmall' - placeholder='Search Projects...' - /> - { - getEnvironments(getStore(), { - projectId: `${project.id}`, - }).then((res: { data: PagedResponse }) => { - router.history.push( - `/project/${project.id}/environment/${ - res.data?.results?.length && res.data?.results?.[0] - ? `${res.data?.results?.[0].api_key}/features` - : 'create' - }`, - ) - }) - }} - /> + {!!open && ( + + {({ user }: { user: User }) => { + return ( +
+
+ + navigateOrganisations(e, user.organisations) + } + onChange={(e) => { + setOrganisationSearch(Utils.safeParseEventValue(e)) + }} + search + inputClassName='border-0 border-bottom-1' + size='xSmall' + className='full-width' + placeholder='Search Organisations...' + /> + { + setHoveredOrganisation(organisation.id) + }} + onClick={(organisation: Organisation) => { + AppActions.selectOrganisation(organisation.id) + AppActions.getOrganisation(organisation.id) + router.history.push(Utils.getOrganisationHomePage()) + }} + /> +
+
+ { + setProjectSearch(Utils.safeParseEventValue(e)) + }} + autoFocus={focus === 'project'} + onKeyDown={(e: InputEvent) => navigateOrganisations(e)} + search + className='full-width' + inputClassName='border-0 border-bottom-1' + size='xSmall' + placeholder='Search Projects...' + /> + { + getEnvironments(getStore(), { + projectId: `${project.id}`, + }).then((res: { data: PagedResponse }) => { + router.history.push( + `/project/${project.id}/environment/${ + res.data?.results?.length && + res.data?.results?.[0] + ? `${res.data?.results?.[0].api_key}/features` + : 'create' + }`, + ) + }) + }} + /> +
-
- ) - }} - + ) + }} + + )}
) From d22158a894e3eb977d0f38aa862673b5fe6eb948 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 14:01:41 +0100 Subject: [PATCH 17/49] Add org filtering --- frontend/web/components/BreadcrumbSeparator.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx index 949fa49a7f8f..920bd034dce5 100644 --- a/frontend/web/components/BreadcrumbSeparator.tsx +++ b/frontend/web/components/BreadcrumbSeparator.tsx @@ -240,6 +240,7 @@ const BreadcrumbSeparator: FC = ({ AppActions.selectOrganisation(organisation.id) AppActions.getOrganisation(organisation.id) router.history.push(Utils.getOrganisationHomePage()) + setOpen(false) }} />
@@ -274,6 +275,7 @@ const BreadcrumbSeparator: FC = ({ : 'create' }`, ) + setOpen(false) }) }} /> From 12920037d23442abc44e06db90951410c0ac7e36 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 14:42:47 +0100 Subject: [PATCH 18/49] Add permissions --- .../common/providers/OrganisationProvider.tsx | 4 +- frontend/common/stores/account-store.js | 6 +- frontend/common/types/responses.ts | 2 + frontend/web/components/OrganisationUsage.tsx | 98 +- frontend/web/components/UserAction.tsx | 2 +- frontend/web/components/UserGroupList.tsx | 2 +- .../pages/OrganisationSettingsPage.js | 56 - .../pages/UsersAndPermissionsPage.tsx | 1443 +++++++++-------- frontend/web/routes.js | 7 +- 9 files changed, 816 insertions(+), 804 deletions(-) diff --git a/frontend/common/providers/OrganisationProvider.tsx b/frontend/common/providers/OrganisationProvider.tsx index 13f0123f4517..9ae19c89637b 100644 --- a/frontend/common/providers/OrganisationProvider.tsx +++ b/frontend/common/providers/OrganisationProvider.tsx @@ -29,7 +29,7 @@ type OrganisationProviderType = { groups: UserGroupSummary[] | null projects: Project[] | null subscriptionMeta: SubscriptionMeta | null - users: User[] | null + users: User[] }) => ReactNode } @@ -77,8 +77,8 @@ const OrganisationProvider: FC = ({ <> {children({ createProject: AppActions.createProject, - groups: groups?.results || [], error: AccountStore.error, + groups: groups?.results || [], invalidateInviteLink: AppActions.invalidateInviteLink, inviteLinks: OrganisationStore.getInviteLinks(), invites: OrganisationStore.getInvites(), diff --git a/frontend/common/stores/account-store.js b/frontend/common/stores/account-store.js index 6db5b6520716..67b375919088 100644 --- a/frontend/common/stores/account-store.js +++ b/frontend/common/stores/account-store.js @@ -306,12 +306,14 @@ const controller = { path: '/organisation/:organisationId', strict: false, })?.params?.organisationId - if (cookiedID) { + const orgId = pathID || cookiedID + if (orgId) { const foundOrganisation = user.organisations.find( - (v) => `${v.id}` === pathID || cookiedID, + (v) => `${v.id}` === orgId, ) if (foundOrganisation) { store.organisation = foundOrganisation + AppActions.getOrganisation(orgId) } } } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index b224ccd18d1f..184f563db631 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -316,6 +316,7 @@ export type githubIntegration = { export type User = { id: number email: string + last_login?: string first_name: string last_name: string role: 'ADMIN' | 'USER' @@ -547,6 +548,7 @@ export type Invite = { email: string date_created: string invited_by: User + link: string permission_groups: number[] } diff --git a/frontend/web/components/OrganisationUsage.tsx b/frontend/web/components/OrganisationUsage.tsx index 82ecbbe261df..c43739aee2e3 100644 --- a/frontend/web/components/OrganisationUsage.tsx +++ b/frontend/web/components/OrganisationUsage.tsx @@ -110,52 +110,58 @@ const OrganisationUsage: FC = ({ organisationId }) => { /> - - - - moment(v).format('D MMM')} - axisLine={{ stroke: '#EFF1F4' }} - tick={{ dx: -4, fill: '#656D7B' }} - tickLine={false} - /> - - <_Tooltip - cursor={{ fill: 'transparent' }} - content={} - /> - - - - - - + {data?.events_list?.length === 0 ? ( +
+ No usage recorded. +
+ ) : ( + + + + moment(v).format('D MMM')} + axisLine={{ stroke: '#EFF1F4' }} + tick={{ dx: -4, fill: '#656D7B' }} + tickLine={false} + /> + + <_Tooltip + cursor={{ fill: 'transparent' }} + content={} + /> + + + + + + + )} Please be aware that usage data can be delayed by up to 3 hours and that these numbers show the API usage for the last 30 days, not your current diff --git a/frontend/web/components/UserAction.tsx b/frontend/web/components/UserAction.tsx index ef60325568b5..7b355bd3d3b6 100644 --- a/frontend/web/components/UserAction.tsx +++ b/frontend/web/components/UserAction.tsx @@ -13,7 +13,7 @@ interface FeatureActionProps { canRemove: boolean onRemove: () => void onEdit: () => void - canEdit: () => void + canEdit: boolean } type ActionType = 'edit' | 'remove' diff --git a/frontend/web/components/UserGroupList.tsx b/frontend/web/components/UserGroupList.tsx index 24ba6466f0af..96d1f3c0dbc9 100644 --- a/frontend/web/components/UserGroupList.tsx +++ b/frontend/web/components/UserGroupList.tsx @@ -19,7 +19,7 @@ import { useGetGroupSummariesQuery } from 'common/services/useGroupSummary' type UserGroupListType = { noTitle?: boolean orgId: string - projectId: string | boolean + projectId?: string | boolean showRemove?: boolean onClick: (group: UserGroup) => void onEditPermissions?: (group: UserGroup) => void diff --git a/frontend/web/components/pages/OrganisationSettingsPage.js b/frontend/web/components/pages/OrganisationSettingsPage.js index 8d6b43f7fa98..25470a92a4fa 100644 --- a/frontend/web/components/pages/OrganisationSettingsPage.js +++ b/frontend/web/components/pages/OrganisationSettingsPage.js @@ -83,36 +83,7 @@ const OrganisationSettingsPage = class extends Component { } } - deleteInvite = (id) => { - openConfirm({ - body: ( -
- Are you sure you want to delete this invite? This action cannot be - undone. -
- ), - destructive: true, - onYes: () => AppActions.deleteInvite(id), - title: 'Delete Invite', - yesText: 'Confirm', - }) - } - deleteUser = (id, userDisplayName) => { - openConfirm({ - body: ( -
- Are you sure you want to remove the user{' '} - {userDisplayName} from the organisation? This action - cannot be undone. -
- ), - destructive: true, - onYes: () => AppActions.deleteUser(id), - title: 'Delete User', - yesText: 'Confirm', - }) - } save = (e) => { e && e.preventDefault() @@ -226,33 +197,6 @@ const OrganisationSettingsPage = class extends Component { ) } - editUserPermissions = (user, organisationId) => { - openModal( - 'Edit Organisation Permissions', -
- -
, - 'p-0 side-modal', - ) - } - formatLastLoggedIn = (last_login) => { - if (!last_login) return 'Never' - - const diff = moment().diff(moment(last_login), 'days') - if (diff >= 30) { - return ( -
- {`${diff} days ago`} -
-
- {moment(last_login).format('Do MMM YYYY')} -
-
- ) - } - return 'Within 30 days' - } - getOrganisationPermissions = (id) => { if (this.state.permissions.length) return diff --git a/frontend/web/components/pages/UsersAndPermissionsPage.tsx b/frontend/web/components/pages/UsersAndPermissionsPage.tsx index e296a6be040b..616e38a2158e 100644 --- a/frontend/web/components/pages/UsersAndPermissionsPage.tsx +++ b/frontend/web/components/pages/UsersAndPermissionsPage.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react' +import React, { FC, useState } from 'react' import classNames from 'classnames' import JSONReference from 'components/JSONReference' import Button from 'components/base/forms/Button' @@ -8,731 +8,792 @@ import Utils from 'common/utils/utils' import Constants from 'common/constants' import ConfigProvider from 'common/providers/ConfigProvider' import InviteUsersModal from 'components/modals/InviteUsers' -import InfoMessage from 'components/InfoMessage'; -import OrganisationProvider from 'common/providers/OrganisationProvider'; -import AccountStore from 'common/stores/account-store'; -import AccountProvider from 'common/providers/AccountProvider'; -import { Invite, InviteLink, Organisation, SubscriptionMeta, User, UserGroupSummary } from 'common/types/responses'; -import CreateGroup from 'components/modals/CreateGroup'; -import UserGroupList from 'components/UserGroupList'; -import { useGetRolesQuery } from 'common/services/useRole'; -import AppActions from 'common/dispatcher/app-actions'; -import { RouterChildContext } from 'react-router'; +import InfoMessage from 'components/InfoMessage' +import OrganisationProvider from 'common/providers/OrganisationProvider' +import AccountStore from 'common/stores/account-store' +import AccountProvider from 'common/providers/AccountProvider' +import { + Invite, + InviteLink, + Organisation, + SubscriptionMeta, + User, + UserGroupSummary, +} from 'common/types/responses' +import CreateGroup from 'components/modals/CreateGroup' +import UserGroupList from 'components/UserGroupList' +import { useGetRolesQuery } from 'common/services/useRole' +import AppActions from 'common/dispatcher/app-actions' +import { RouterChildContext } from 'react-router' +import map from 'lodash/map' +import Input from 'components/base/forms/Input' +import ErrorMessage from 'components/ErrorMessage' +import PanelSearch from 'components/PanelSearch' +import Format from 'common/utils/format' +import moment from 'moment' +import PermissionsTabs from 'components/PermissionsTabs' +import sortBy from 'lodash/sortBy' +import UserAction from 'components/UserAction' +import Icon from 'components/Icon' +import RolesTable from 'components/RolesTable'; -type UsersAndPermissionsPageType = {} +type UsersAndPermissionsPageType = { + router: RouterChildContext['router'] +} const widths = [300, 200, 80] - - type UsersAndPermissionsInnerType = { - organisation: Organisation - error: any - invalidateInviteLink: typeof AppActions.invalidateInviteLink - inviteLinks: InviteLink[] | null - invites: Invite[] | null - isLoading:boolean - users: User[] | null - subscriptionMeta: SubscriptionMeta | null - router: RouterChildContext['router'] + organisation: Organisation + error: any + invalidateInviteLink: typeof AppActions.invalidateInviteLink + inviteLinks: InviteLink[] | null + invites: Invite[] | null + isLoading: boolean + users: User[] + subscriptionMeta: SubscriptionMeta | null + router: RouterChildContext['router'] } const UsersAndPermissionsInner: FC = ({ -router, - organisation, - error, - invalidateInviteLink, - inviteLinks, -invites, -isLoading, -users, -subscriptionMeta, - }) => { - const permissionsError = !( - AccountStore.getUser() && - AccountStore.getOrganisationRole() === 'ADMIN' + error, + invalidateInviteLink, + inviteLinks, + invites, + isLoading, + organisation, + router, + subscriptionMeta, + users, +}) => { + const orgId = AccountStore.getOrganisation().id + + const paymentsEnabled = Utils.getFlagsmithHasFeature('payments_enabled') + const verifySeatsLimit = Utils.getFlagsmithHasFeature( + 'verify_seats_limit_for_invite_links', + ) + const permissionsError = !( + AccountStore.getUser() && AccountStore.getOrganisationRole() === 'ADMIN' + ) + const roleChanged = (id: number, { value: role }: { value: string }) => { + AppActions.updateUserRole(id, role) + } + const { data: roles } = useGetRolesQuery({ organisation_id: organisation.id }) + + const editGroup = (group: UserGroupSummary) => { + openModal( + 'Edit Group', + , + 'side-modal', ) - const roleChanged = (id:string, { value: role }:{value:string}) => { - AppActions.updateUserRole(id, role) - } - const {data:roles} = useGetRolesQuery({organisation_id: organisation.id}) - const paymentsEnabled = Utils.getFlagsmithHasFeature('payments_enabled') - const verifySeatsLimit = Utils.getFlagsmithHasFeature( - 'verify_seats_limit_for_invite_links', + } + const hasRbacPermission = Utils.getPlansPermission('RBAC') + const meta = subscriptionMeta || organisation.subscription || { max_seats: 1 } + const max_seats = meta.max_seats || 1 + const isAWS = AccountStore.getPaymentMethod() === 'AWS_MARKETPLACE' + const autoSeats = !isAWS && Utils.getPlansPermission('AUTO_SEATS') + const usedSeats = paymentsEnabled && organisation.num_seats >= max_seats + const overSeats = paymentsEnabled && organisation.num_seats > max_seats + const [role, setRole] = useState<'ADMIN' | 'USER'>('ADMIN') + + const editUserPermissions = (user: User, organisationId: number) => { + openModal( + 'Edit Organisation Permissions', +
+ +
, + 'p-0 side-modal', ) - const meta = subscriptionMeta || - organisation.subscription || { max_seats: 1 } - const max_seats = meta.max_seats || 1 - const isAWS = - AccountStore.getPaymentMethod() === 'AWS_MARKETPLACE' - const autoSeats = - !isAWS && Utils.getPlansPermission('AUTO_SEATS') - const usedSeats = - paymentsEnabled && organisation.num_seats >= max_seats - const overSeats = - paymentsEnabled && organisation.num_seats > max_seats + } + const formatLastLoggedIn = (last_login: string | undefined) => { + if (!last_login) return 'Never' - const needsUpgradeForAdditionalSeats = - (overSeats && (!verifySeatsLimit || !autoSeats)) || - (!autoSeats && usedSeats) - return ( - <> - - + const diff = moment().diff(moment(last_login), 'days') + if (diff >= 30) { + return ( +
+ {`${diff} days ago`} +
+
+ {moment(last_login).format('Do MMM YYYY')} +
+
+ ) + } + return 'Within 30 days' + } - -
-
Manage Users and Permissions
-

- Flagsmith lets you manage fine-grained permissions for your projects - and environments, invite members as a user or an administrator and - then set permission in your Project and Environment settings.{' '} - -

+ const deleteUser = (id: number, userDisplayName: string) => { + openConfirm({ + body: ( +
+ Are you sure you want to remove the user{' '} + {userDisplayName} from the organisation? This action + cannot be undone. +
+ ), + destructive: true, + onYes: () => AppActions.deleteUser(id), + title: 'Delete User', + yesText: 'Confirm', + }) + } + const deleteInvite = (id: number) => { + openConfirm({ + body: ( +
+ Are you sure you want to delete this invite? This action cannot be + undone. +
+ ), + destructive: true, + onYes: () => AppActions.deleteInvite(id), + title: 'Delete Invite', + yesText: 'Confirm', + }) + } + const needsUpgradeForAdditionalSeats = + (overSeats && (!verifySeatsLimit || !autoSeats)) || + (!autoSeats && usedSeats) + return ( +
+ + + + +
+
Manage Users and Permissions
+

+ Flagsmith lets you manage fine-grained permissions for your projects + and environments, invite members as a user or an administrator and + then set permission in your Project and Environment settings.{' '} + +

+
+
+
+
+ {isLoading && ( +
+
+ )} + {!isLoading && (
-
-
- {isLoading && ( -
- -
- )} - {!isLoading && ( -
- - - -
Team Members
- {Utils.renderWithPermission( - !permissionsError, - Constants.organisationPermissions('Admin'), - , - )} -
- - {paymentsEnabled && !isLoading && ( -
- - {'You are currently using '} - - {`${organisation.num_seats} of ${max_seats}`} - - {` seat${ - organisation.num_seats === 1 ? '' : 's' - } `}{' '} - for your plan.{' '} - {usedSeats && ( - <> - {overSeats && - (!verifySeatsLimit || !autoSeats) ? ( - - If you wish to invite any additional - members, please{' '} - { - - Contact us - - } - . - - ) : needsUpgradeForAdditionalSeats ? ( - - If you wish to invite any additional - members, please{' '} - { - { - router.history.replace( - `${ - document.location.pathname - }?${Utils.toParam({ - ...Utils.fromParam(), - tab: this.state.tab - ? 'closed' - : 'open', - })}`, - ) - }} - > - Upgrade your plan - - } - . - - ) : ( - - You will automatically be charged - $20/month for each additional member that - joins your organisation. - - )} - - )} - -
- )} - {inviteLinks && - (!verifySeatsLimit || - !needsUpgradeForAdditionalSeats) && ( -
{ - e.preventDefault() - }} - > -
- -
- f.role === this.state.role, - ).hash - }`} - data-test='invite-link' - inputClassName='input input--wide' - type='text' - readonly='readonly' - title={

Link

} - placeholder='Link' - size='small' - /> - - - - - - - )} - -
-

- Anyone with link can join as a standard user, - once they have joined you can edit their role - from the team members panel.{' '} - -

-
- {error && } -
- - )} - - User -
- Role -
-
- Last logged in -
-
- Actions -
-
- } - items={users} - itemHeight={65} - renderRow={(user, i) => { - const { - email, - first_name, - id, - last_login, - last_name, - role, - } = user + + + +
Team Members
+ {Utils.renderWithPermission( + !permissionsError, + Constants.organisationPermissions('Admin'), + , + )} +
+ + {paymentsEnabled && !isLoading && ( +
+ + {'You are currently using '} + + {`${organisation.num_seats} of ${max_seats}`} + + {` seat${ + organisation.num_seats === 1 ? '' : 's' + } `}{' '} + for your plan.{' '} + {usedSeats && ( + <> + {overSeats && + (!verifySeatsLimit || !autoSeats) ? ( + + If you wish to invite any additional + members, please{' '} + { + + Contact us + + } + . + + ) : needsUpgradeForAdditionalSeats ? ( + + If you wish to invite any additional + members, please{' '} + { + { + router.history.replace( + Constants.upgradeURL, + ) + }} + > + Upgrade your plan + + } + . + + ) : ( + + You will automatically be charged + $20/month for each additional member that + joins your organisation. + + )} + + )} + +
+ )} + {inviteLinks && + (!verifySeatsLimit || + !needsUpgradeForAdditionalSeats) && ( +
{ + e.preventDefault() + }} + > +
+ +
+ f.role === role, + )?.hash + }`} + data-test='invite-link' + inputClassName='input input--wide' + type='text' + readonly='readonly' + title={

Link

} + placeholder='Link' + size='small' + /> + + + + + + + )} + +
+

+ Anyone with link can join as a standard user, + once they have joined you can edit their role + from the team members panel.{' '} + +

+
+ {error && } +
+ + )} + + User +
+ Role +
+
+ Last logged in +
+
+ Actions +
+
+ } + items={users} + itemHeight={65} + renderRow={(user: User, i: number) => { + const { + email, + first_name, + id, + last_login, + last_name, + role, + } = user - const onRemoveClick = () => { - this.deleteUser( - id, - Format.userDisplayName({ - email, - firstName: first_name, - lastName: last_name, - }), - email, - ) - } - const onEditClick = () => { - if (role !== 'ADMIN') { - this.editUserPermissions(user, organisation.id) - } - } - return ( - - - {`${first_name} ${last_name}`}{' '} - {id === AccountStore.getUserId() && '(You)'} -
- {email} -
-
+ const onRemoveClick = () => { + deleteUser( + id, + Format.userDisplayName({ + email, + firstName: first_name, + lastName: last_name, + }), + ) + } + const onEditClick = () => { + if (role !== 'ADMIN') { + editUserPermissions(user, organisation.id) + } + } + return ( + + + {`${first_name} ${last_name}`}{' '} + {id === AccountStore.getUserId() && '(You)'} +
+ {email} +
+
-
-
- {organisation.role === 'ADMIN' && - id !== AccountStore.getUserId() ? ( -
- + roleChanged( + id, + Utils.safeParseEventValue(e), + ) + } + options={map( + Constants.roles, + (label, value) => ({ + isDisabled: + value !== 'ADMIN' && + !hasRbacPermission, + label: + value !== 'ADMIN' && + !hasRbacPermission + ? `${label} - Please upgrade for role based access` + : label, + value, + }), + )} + menuPortalTarget={document.body} + menuPosition='absolute' + menuPlacement='auto' + className='react-select select-xsm' + /> +
+ ) : ( +
+ {Constants.roles[role] || ''} +
+ )} +
+
+
+
+ {formatLastLoggedIn(last_login)} +
+
+
+ +
+
+ ) + }} + renderNoResults={ +
You have no users in this organisation.
+ } + filterRow={(item: User, search: string) => { + const strToSearch = `${item.first_name} ${item.last_name} ${item.email}` + return ( + strToSearch + .toLowerCase() + .indexOf(search.toLowerCase()) !== -1 + ) + }} + /> +
+ - {invites && invites.length ? ( - - - User -
- Role -
-
- Action -
- - } - renderRow={( - { date_created, email, id, invited_by, link }, - i, - ) => ( - -
- {email || link} -
- Created{' '} - {moment(date_created).format('DD/MMM/YYYY')} -
- {invited_by ? ( -
- Invited by{' '} - {invited_by.first_name - ? `${invited_by.first_name} ${invited_by.last_name}` - : invited_by.email} -
- ) : null} -
-
- {link ? ( - ' ' - ) : ( - - )} -
-
- -
-
- )} - filterRow={(item, search) => - item.email - .toLowerCase() - .indexOf(search.toLowerCase()) !== -1 - } - /> -
- ) : null} - - -
- -
User Groups
- {Utils.renderWithPermission( - !permissionsError, - Constants.organisationPermissions('Admin'), - , - )} -
-

- Groups allow you to manage permissions for viewing and - editing projects, features and environments. -

- -
-
- {Utils.getFlagsmithHasFeature('show_role_management') && ( - - {hasRbacPermission ? ( - <> - - - ) : ( -
- - To use role features you have to - upgrade your plan. - -
- )} -
- )} - + {invites && invites.length ? ( + + + User +
+ Role +
+
+ Action +
+ + } + renderRow={( + { + date_created, + email, + id, + invited_by, + link, + }: Invite, + i: number, + ) => ( + +
+ {email || link} +
+ Created{' '} + {moment(date_created).format('DD/MMM/YYYY')} +
+ {invited_by ? ( +
+ Invited by{' '} + {invited_by.first_name + ? `${invited_by.first_name} ${invited_by.last_name}` + : invited_by.email} +
+ ) : null}
+
+ {link ? ( + ' ' + ) : ( + + )} +
+
+ +
+
)} -
-
+ filterRow={(item: Invite, search: string) => + item.email + .toLowerCase() + .indexOf(search.toLowerCase()) !== -1 + } + /> +
+ ) : null} +
+ +
+ +
User Groups
+ {Utils.renderWithPermission( + !permissionsError, + Constants.organisationPermissions('Admin'), + , + )} +
+

+ Groups allow you to manage permissions for viewing and + editing projects, features and environments. +

+ +
+
+ {Utils.getFlagsmithHasFeature('show_role_management') && ( + + {hasRbacPermission ? ( + <> + + + ) : ( +
+ + To use role features you have to + upgrade your plan. + +
+ )} +
+ )} +
-
- - ) -}; - -const UsersAndPermissionsPage: FC = ({}) => { - - const orgId = AccountStore.getOrganisation().id - - const paymentsEnabled = Utils.getFlagsmithHasFeature('payments_enabled') - const verifySeatsLimit = Utils.getFlagsmithHasFeature( - 'verify_seats_limit_for_invite_links', - ) + )} +
+
+
+ +
+ ) +} - const editGroup = (group:UserGroupSummary) => { - openModal( - 'Edit Group', - , - 'side-modal', - ) - } +const UsersAndPermissionsPage: FC = ({ + router, +}) => { return ( - - {({ organisation }:{organisation:Organisation|null}) => ( - - {({ + + {({ organisation }: { organisation: Organisation | null }) => ( + + {({ error, invalidateInviteLink, inviteLinks, invites, isLoading, - users, subscriptionMeta, + users, }) => { - - if(!organisation) { - return
+ if (!organisation) { + return ( +
+ +
+ ) } - return - ) - }} - -
- )} -
- + /> + ) + }} +
+ )} +
) } diff --git a/frontend/web/routes.js b/frontend/web/routes.js index 52eacbadb506..dd7711867a85 100644 --- a/frontend/web/routes.js +++ b/frontend/web/routes.js @@ -36,6 +36,7 @@ import OrganisationSettingsRedirectPage from './components/pages/OrganisationSet import OrganisationUsagePage from './components/pages/OrganisationUsagePage' import OrganisationsPage from './components/pages/OrganisationsPage' import PageTitle from './components/PageTitle' +import UsersAndPermissionsPage from './components/pages/UsersAndPermissionsPage'; export default ( @@ -149,11 +150,7 @@ export default ( ( -
- -
- )} + component={UsersAndPermissionsPage} /> Date: Wed, 1 May 2024 14:58:35 +0100 Subject: [PATCH 19/49] Add Project homepage --- .../web/components/BreadcrumbSeparator.tsx | 9 +-- frontend/web/components/EditPermissions.tsx | 9 +++ .../components/OrganisationManageWidget.tsx | 62 ------------------- .../pages/OrganisationSettingsPage.js | 6 +- .../components/pages/ProjectRedirectPage.tsx | 46 ++++++++++++++ frontend/web/routes.js | 4 +- 6 files changed, 60 insertions(+), 76 deletions(-) delete mode 100644 frontend/web/components/OrganisationManageWidget.tsx create mode 100644 frontend/web/components/pages/ProjectRedirectPage.tsx diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx index 920bd034dce5..ae49d58d8036 100644 --- a/frontend/web/components/BreadcrumbSeparator.tsx +++ b/frontend/web/components/BreadcrumbSeparator.tsx @@ -267,14 +267,7 @@ const BreadcrumbSeparator: FC = ({ getEnvironments(getStore(), { projectId: `${project.id}`, }).then((res: { data: PagedResponse }) => { - router.history.push( - `/project/${project.id}/environment/${ - res.data?.results?.length && - res.data?.results?.[0] - ? `${res.data?.results?.[0].api_key}/features` - : 'create' - }`, - ) + router.history.push(`/project/${project.id}`) setOpen(false) }) }} diff --git a/frontend/web/components/EditPermissions.tsx b/frontend/web/components/EditPermissions.tsx index f62410437b23..29ffa7bf4240 100644 --- a/frontend/web/components/EditPermissions.tsx +++ b/frontend/web/components/EditPermissions.tsx @@ -721,6 +721,15 @@ const _EditPermissionsModal: FC = withAdminPermissions(
)} + {!hasRbacPermission && ( + + Role-based access is not available on our Free Plan. Please visit{' '} + + our Pricing Page + {' '} + for more information on our licensing options. + + )} { const name = Format.enumeration.get(item.key).toLowerCase() diff --git a/frontend/web/components/OrganisationManageWidget.tsx b/frontend/web/components/OrganisationManageWidget.tsx deleted file mode 100644 index 41a3975a46ea..000000000000 --- a/frontend/web/components/OrganisationManageWidget.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { FC, useCallback, useEffect } from 'react' - -import AccountProvider from 'common/providers/AccountProvider' -import AccountStore from 'common/stores/account-store' -import AppActions from 'common/dispatcher/app-actions' -import Utils from 'common/utils/utils' -import Project from 'common/project' -import Button from './base/forms/Button' -import CreateOrganisationModal from './modals/CreateOrganisation' -import Icon from './Icon' -import OrganisationSelect from './OrganisationSelect' - -type OrganisationManageWidgetType = { - onChange?: () => void -} - -const OrganisationManageWidget: FC = ({ - onChange, -}) => { - const handleCreateOrganisationClick = useCallback(() => { - openModal('Create Organisation', , 'side-modal') - }, []) - - useEffect(() => { - AppActions.getOrganisation(AccountStore.getOrganisation().id) - }, []) - - return ( - - onChange && onChange()}> - {({ organisation }: { organisation: unknown }) => - organisation && ( - { - AppActions.selectOrganisation(organisationId) - AppActions.getOrganisation(organisationId) - }} - /> - ) - } - - {!Utils.getFlagsmithHasFeature('disable_create_org') && - (!Project.superUserCreateOnly || - (Project.superUserCreateOnly && AccountStore.isSuper())) && ( -
- - - -
- )} -
- ) -} - -export default OrganisationManageWidget diff --git a/frontend/web/components/pages/OrganisationSettingsPage.js b/frontend/web/components/pages/OrganisationSettingsPage.js index 25470a92a4fa..4ad992784920 100644 --- a/frontend/web/components/pages/OrganisationSettingsPage.js +++ b/frontend/web/components/pages/OrganisationSettingsPage.js @@ -12,11 +12,9 @@ import JSONReference from 'components/JSONReference' import ConfigProvider from 'common/providers/ConfigProvider' import Constants from 'common/constants' import Icon from 'components/Icon' -import OrganisationManageWidget from 'components/OrganisationManageWidget' import _data from 'common/data/base/_data' -import PermissionsTabs from 'components/PermissionsTabs' import AccountStore from 'common/stores/account-store' -import PageTitle from '../PageTitle'; +import PageTitle from 'components/PageTitle' const SettingsTab = { 'Billing': 'billing', @@ -83,8 +81,6 @@ const OrganisationSettingsPage = class extends Component { } } - - save = (e) => { e && e.preventDefault() const { diff --git a/frontend/web/components/pages/ProjectRedirectPage.tsx b/frontend/web/components/pages/ProjectRedirectPage.tsx new file mode 100644 index 000000000000..1d64117ec28e --- /dev/null +++ b/frontend/web/components/pages/ProjectRedirectPage.tsx @@ -0,0 +1,46 @@ +import { FC, useEffect } from 'react' +import { useGetEnvironmentsQuery } from 'common/services/useEnvironment' +import { RouterChildContext } from 'react-router' +import Utils from 'common/utils/utils' +import ConfigProvider from 'common/providers/ConfigProvider'; + +type ProjectRedirectPageType = { + router: RouterChildContext['router'] + match: { + params: { + projectId: string + } + } +} + +const ProjectRedirectPage: FC = ({ + match: { + params: { projectId }, + }, + router, +}) => { + const { data, error } = useGetEnvironmentsQuery({ projectId }) + useEffect(() => { + if (!data) { + return + } + if (error) { + router.history.replace(Utils.getOrganisationHomePage()) + } + const environment = data?.results?.[0] + if (environment) { + router.history.replace( + `/project/${projectId}/environment/${environment.api_key}/features`, + ) + } else { + router.history.replace(`/project/${projectId}/environment/create`) + } + }, [data, error]) + return ( +
+ +
+ ) +} + +export default ConfigProvider(ProjectRedirectPage) diff --git a/frontend/web/routes.js b/frontend/web/routes.js index dd7711867a85..8b1c506e405b 100644 --- a/frontend/web/routes.js +++ b/frontend/web/routes.js @@ -36,7 +36,8 @@ import OrganisationSettingsRedirectPage from './components/pages/OrganisationSet import OrganisationUsagePage from './components/pages/OrganisationUsagePage' import OrganisationsPage from './components/pages/OrganisationsPage' import PageTitle from './components/PageTitle' -import UsersAndPermissionsPage from './components/pages/UsersAndPermissionsPage'; +import UsersAndPermissionsPage from './components/pages/UsersAndPermissionsPage' +import ProjectRedirectPage from './components/pages/ProjectRedirectPage' export default ( @@ -172,6 +173,7 @@ export default ( exact component={AccountSettingsPage} /> + Date: Wed, 1 May 2024 14:59:33 +0100 Subject: [PATCH 20/49] Fix project link --- frontend/web/components/App.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index e49696175978..c9de7c19607d 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -477,7 +477,7 @@ const App = class extends Component { focus='project' > Date: Wed, 1 May 2024 15:26:59 +0100 Subject: [PATCH 21/49] Keyed nav in BreadcrumbSeparator --- .../web/components/BreadcrumbSeparator.tsx | 111 +++++++++++++----- frontend/web/styles/project/_project-nav.scss | 4 +- 2 files changed, 86 insertions(+), 29 deletions(-) diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx index ae49d58d8036..57242e51fc88 100644 --- a/frontend/web/components/BreadcrumbSeparator.tsx +++ b/frontend/web/components/BreadcrumbSeparator.tsx @@ -38,9 +38,10 @@ type BreadcrumbSeparatorType = { type ItemListType = { items: any[] | undefined - onHover: (item: any) => void + onHover?: (item: any) => void onClick: (item: any) => void value?: any + hoverValue?: any isLoading?: boolean className?: string title: string @@ -49,6 +50,7 @@ type ItemListType = { const ItemList: FC = ({ className, + hoverValue, isLoading, items: _items, onClick, @@ -84,6 +86,7 @@ const ItemList: FC = ({ ) : null} {items?.map((v) => { const isActive = `${v.id}` === `${value}` + const isHovered = `${v.id}` === `${hoverValue}` return ( onHover?.(v)} @@ -92,6 +95,7 @@ const ItemList: FC = ({ className={classNames( 'breadcrumb-link py-2 d-flex align-items-center justify-content-between', { active: isActive }, + { hovered: isHovered }, )} > {v.name} @@ -117,11 +121,14 @@ const BreadcrumbSeparator: FC = ({ const [organisationSearch, setOrganisationSearch] = useState('') const [projectSearch, setProjectSearch] = useState('') - const [activeOrganisation, setActiveOrganisation] = useState( - AccountStore.getOrganisation()?.id, + const [activeOrganisation, setActiveOrganisation] = useState( + `${AccountStore.getOrganisation()?.id}`, ) - const [hoveredOrganisation, setHoveredOrganisation] = useState( - AccountStore.getOrganisation()?.id, + const [hoveredOrganisation, setHoveredOrganisation] = useState( + `${AccountStore.getOrganisation()?.id}`, + ) + const [hoveredProject, setHoveredProject] = useState( + projectId, ) useEffect(() => { @@ -151,10 +158,68 @@ const BreadcrumbSeparator: FC = ({ ) const navigateOrganisations = ( - e: InputEvent, + e: KeyboardEvent, organisations: Organisation[], - ) => {} - const navigateProjects = (e: InputEvent) => {} + ) => { + const currentIndex = organisations + ? organisations.findIndex((v) => `${v.id}` === `${hoveredOrganisation}`) + : -1 + const newIndex = getNewIndex(e, currentIndex, organisations, goOrganisation) + if (newIndex > -1) { + setHoveredProject(undefined) + setHoveredOrganisation(`${organisations![newIndex]!.id}`) + } + } + + const getNewIndex = ( + e: KeyboardEvent, + currentIndex: number, + items: any[] | undefined, + go: (item: any) => void, + ) => { + if (!items?.length) { + return -1 + } + + if (e.key === 'Enter' && items[currentIndex]) { + go(items[currentIndex]) + return currentIndex + } + if (e.key === 'ArrowDown') { + if (currentIndex + 1 < items?.length) { + return currentIndex + 1 + } else { + return items?.length - 1 + } + } else if (e.key === 'ArrowUp') { + return Math.max(-1, currentIndex - 1) + } + + return -1 + } + const navigateProjects = (e: KeyboardEvent) => { + const currentIndex = projects + ? projects.findIndex((v) => `${v.id}` === `${hoveredProject}`) + : -1 + const newIndex = getNewIndex(e, currentIndex, projects, goProject) + if (newIndex > -1) { + setHoveredProject(`${projects![newIndex]!.id}`) + } + } + const goOrganisation = (organisation: Organisation) => { + AppActions.selectOrganisation(organisation.id) + AppActions.getOrganisation(organisation.id) + router.history.push(Utils.getOrganisationHomePage()) + setOpen(false) + } + const goProject = (project: Project) => { + getEnvironments(getStore(), { + projectId: `${project.id}`, + }).then((res: { data: PagedResponse }) => { + router.history.push(`/project/${project.id}`) + setOpen(false) + }) + } return (
{children} @@ -215,10 +280,10 @@ const BreadcrumbSeparator: FC = ({
+ onKeyDown={(e: KeyboardEvent) => navigateOrganisations(e, user.organisations) } - onChange={(e) => { + onChange={(e: KeyboardEvent) => { setOrganisationSearch(Utils.safeParseEventValue(e)) }} search @@ -231,26 +296,23 @@ const BreadcrumbSeparator: FC = ({ search={organisationSearch} className='px-2 pt-2' title='Organisations' + hoverValue={hoveredOrganisation} items={user.organisations} value={activeOrganisation} onHover={(organisation: Organisation) => { - setHoveredOrganisation(organisation.id) - }} - onClick={(organisation: Organisation) => { - AppActions.selectOrganisation(organisation.id) - AppActions.getOrganisation(organisation.id) - router.history.push(Utils.getOrganisationHomePage()) - setOpen(false) + setHoveredOrganisation(`${organisation.id}`) + setHoveredProject(undefined) }} + onClick={goOrganisation} />
{ + onChange={(e: InputEvent) => { setProjectSearch(Utils.safeParseEventValue(e)) }} autoFocus={focus === 'project'} - onKeyDown={(e: InputEvent) => navigateOrganisations(e)} + onKeyDown={(e: KeyboardEvent) => navigateProjects(e)} search className='full-width' inputClassName='border-0 border-bottom-1' @@ -263,14 +325,9 @@ const BreadcrumbSeparator: FC = ({ title='Projects' items={projects} value={projectId} - onClick={(project: Project) => { - getEnvironments(getStore(), { - projectId: `${project.id}`, - }).then((res: { data: PagedResponse }) => { - router.history.push(`/project/${project.id}`) - setOpen(false) - }) - }} + hoverValue={hoveredProject} + onHover={(v) => setHoveredProject(v.id)} + onClick={goProject} />
diff --git a/frontend/web/styles/project/_project-nav.scss b/frontend/web/styles/project/_project-nav.scss index 287fb7b92311..68c9ff8dbb60 100644 --- a/frontend/web/styles/project/_project-nav.scss +++ b/frontend/web/styles/project/_project-nav.scss @@ -36,13 +36,13 @@ nav a { .breadcrumb-link { padding: 4px 8px 4px 8px; border-radius: $border-radius; - &:hover { + &:hover,&.hovered { color: $body-text; background-color: $primary-alfa-8; } } .dark { - .breadcrumb-link:hover { + .breadcrumb-link:hover, .breadcrumb-link.hovered { color: $body-text-dark; } .nav-sub-link { From f6d1ee839b6919525aea25c86ae3a0ed41fd2bd1 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 15:40:31 +0100 Subject: [PATCH 22/49] Follow keyboard --- .../web/components/BreadcrumbSeparator.tsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/frontend/web/components/BreadcrumbSeparator.tsx b/frontend/web/components/BreadcrumbSeparator.tsx index 57242e51fc88..d97903229240 100644 --- a/frontend/web/components/BreadcrumbSeparator.tsx +++ b/frontend/web/components/BreadcrumbSeparator.tsx @@ -1,4 +1,4 @@ -import { FC, ReactNode, useEffect, useState } from 'react' +import { FC, ReactNode, useEffect, useRef, useState } from 'react' import { IonIcon } from '@ionic/react' import { checkmark, @@ -64,8 +64,29 @@ const ItemList: FC = ({ return v.name.toLowerCase().includes(search.toLowerCase()) }) : _items + const ref = useRef(Utils.GUID()) + useEffect(() => { + const index = items?.findIndex((v) => `${v.id}` === `${hoverValue}`) + const el = document.getElementById(ref.current) + const childEl = document.getElementById(`${ref.current}-${index}`) + if (el && childEl) { + const containerBounds = el.getBoundingClientRect() + const elementBounds = childEl.getBoundingClientRect() + const isInView = + elementBounds.top >= containerBounds.top && + elementBounds.bottom <= containerBounds.bottom + if (!isInView) { + // Calculate how much to scroll the container to bring the element into view + const scrollAmount = elementBounds.top - containerBounds.top + + // Scroll the container + el.scrollTop += scrollAmount + } + } + }, [hoverValue]) return (
@@ -84,11 +105,12 @@ const ItemList: FC = ({
No Results
) ) : null} - {items?.map((v) => { + {items?.map((v, i) => { const isActive = `${v.id}` === `${value}` const isHovered = `${v.id}` === `${hoverValue}` return (
onHover?.(v)} onClick={() => onClick(v)} key={v.id} From 83167df7a7f1af2401a053b07fadd86860a998d8 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 15:51:06 +0100 Subject: [PATCH 23/49] prevent cache misses --- frontend/common/stores/organisation-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index 977fac78fb43..07f252d2ce2e 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -125,7 +125,7 @@ const controller = { }) }, getOrganisation: (id, force) => { - if (id !== store.id || force) { + if (`${id}` !== `${store.id}` || force) { store.id = id store.loading() From 23e9d3763410b17f5f297a60a8e7d7b0bfece43b Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 15:51:47 +0100 Subject: [PATCH 24/49] prevent cache misses --- frontend/common/stores/organisation-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index 07f252d2ce2e..b282fd432072 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -146,7 +146,7 @@ const controller = { : [], ), ).then((res) => { - if (id === store.id) { + if (`${id}` === `${store.id}`) { // eslint-disable-next-line prefer-const let [_projects, users, invites, subscriptionMeta] = res let projects = _.sortBy(_projects, 'name') From 8933cbea8ec60a7986c1f0e990aca197f3c5dc6f Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 1 May 2024 18:54:59 +0100 Subject: [PATCH 25/49] Fix tests --- frontend/common/stores/organisation-store.js | 1 + frontend/common/stores/project-store.js | 1 + frontend/e2e/helpers.cafe.ts | 3 + frontend/e2e/tests/initialise-tests.ts | 1 + frontend/e2e/tests/invite-test.ts | 1 + frontend/e2e/tests/project-test.ts | 7 +- frontend/e2e/tests/versioning-tests.ts | 1 - frontend/web/components/App.js | 14 +- frontend/web/components/Aside.js | 663 ------------------ frontend/web/components/ButterBar.tsx | 11 +- frontend/web/components/OrganisationLimit.tsx | 9 +- frontend/web/components/OrganisationUsage.tsx | 17 +- .../web/components/ProjectManageWidget.tsx | 1 - frontend/web/components/pages/HomeAside.tsx | 1 + .../components/pages/OrganisationsPage.tsx | 8 +- frontend/web/static/images/nav-logo-small.svg | 1 - frontend/web/static/images/nav-logo.png | Bin 0 -> 8003 bytes frontend/web/static/images/nav-logo.svg | 1 - 18 files changed, 51 insertions(+), 690 deletions(-) delete mode 100644 frontend/web/components/Aside.js delete mode 100644 frontend/web/static/images/nav-logo-small.svg create mode 100644 frontend/web/static/images/nav-logo.png delete mode 100644 frontend/web/static/images/nav-logo.svg diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index b282fd432072..55ec16edcc1e 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -57,6 +57,7 @@ const controller = { environmentId: res[0].api_key, projectId: project.id, } + AppActions.refreshOrganisation() store.saved() }) }) diff --git a/frontend/common/stores/project-store.js b/frontend/common/stores/project-store.js index 7c3a2f9bd4c3..0fe419143906 100644 --- a/frontend/common/stores/project-store.js +++ b/frontend/common/stores/project-store.js @@ -91,6 +91,7 @@ const controller = { data.put(`${Project.api}projects/${project.id}/`, project).then((res) => { store.model = Object.assign(store.model, res) getStore().dispatch(projectService.util.invalidateTags(['Project'])) + AppActions.refreshOrganisation() store.saved() }) }, diff --git a/frontend/e2e/helpers.cafe.ts b/frontend/e2e/helpers.cafe.ts index 11184e16be28..300f9aede73b 100644 --- a/frontend/e2e/helpers.cafe.ts +++ b/frontend/e2e/helpers.cafe.ts @@ -103,6 +103,7 @@ export const getLogger = () => }) export const gotoTraits = async () => { + await click('#features-link') await click('#users-link') await click(byId('user-item-0')) await waitForElementVisible('#add-trait') @@ -219,6 +220,7 @@ export const saveFeatureSegments = async () => { } export const goToUser = async (index: number) => { + await click('#features-link') await click('#users-link') await click(byId(`user-item-${index}`)) } @@ -331,6 +333,7 @@ export const createRemoteConfig = async ( export const createOrganisationAndProject = async (organisationName:string,projectName:string) =>{ log('Create Organisation') + await click(byId('home-link')) await click(byId('create-organisation-btn')) await setText('[name="orgName"]', organisationName) await click('#create-org-btn') diff --git a/frontend/e2e/tests/initialise-tests.ts b/frontend/e2e/tests/initialise-tests.ts index 91fafe9d7832..348628bb6d74 100644 --- a/frontend/e2e/tests/initialise-tests.ts +++ b/frontend/e2e/tests/initialise-tests.ts @@ -26,6 +26,7 @@ export default async function () { await waitForElementVisible(byId('features-page')) log('Hide disabled flags') + await click('#project-link') await click('#project-settings-link') await click(byId('js-sdk-settings')) await click(byId('js-hide-disabled-flags')) diff --git a/frontend/e2e/tests/invite-test.ts b/frontend/e2e/tests/invite-test.ts index 253133c8589b..6a1502647535 100644 --- a/frontend/e2e/tests/invite-test.ts +++ b/frontend/e2e/tests/invite-test.ts @@ -18,6 +18,7 @@ export default async function () { log('Get Invite url') await t.navigateTo('http://localhost:3000/organisation-settings') await Selector(byId('organisation-name')).value + await click(byId('users-and-permissions')) const inviteLink = await Selector(byId('invite-link')).value log('Accept invite') await t.navigateTo(inviteLink) diff --git a/frontend/e2e/tests/project-test.ts b/frontend/e2e/tests/project-test.ts index bfe63a0f60f7..48252d2c712b 100644 --- a/frontend/e2e/tests/project-test.ts +++ b/frontend/e2e/tests/project-test.ts @@ -1,11 +1,12 @@ import { + assertTextContent, byId, click, log, login, setText, waitForElementVisible, -} from '../helpers.cafe' +} from '../helpers.cafe'; import { E2E_USER, PASSWORD } from '../config' export default async function () { @@ -13,8 +14,10 @@ export default async function () { await login(E2E_USER, PASSWORD) await click('#project-select-0') log('Edit Project') + await click('#project-link') await click('#project-settings-link') await setText("[name='proj-name']", 'Test Project') await click('#save-proj-btn') - await waitForElementVisible(byId('switch-project-test-project')) + await assertTextContent(`#project-link`, 'Test Project') + } diff --git a/frontend/e2e/tests/versioning-tests.ts b/frontend/e2e/tests/versioning-tests.ts index 7d395791309b..20be8b42c21e 100644 --- a/frontend/e2e/tests/versioning-tests.ts +++ b/frontend/e2e/tests/versioning-tests.ts @@ -9,7 +9,6 @@ import { editRemoteConfig, log, login, refreshUntilElementVisible, - waitAndRefresh, waitForElementVisible } from "../helpers.cafe"; import { E2E_USER, PASSWORD } from '../config'; diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index c9de7c19607d..5232bea23c2a 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -320,7 +320,7 @@ const App = class extends Component { const projectNotLoaded = !activeProject && document.location.href.includes('project/') - if (this.props.isLoading || projectNotLoaded) { + if (this.props.isLoading) { return (
- - -
Client-side Environment Key
-
-

- Use this key to initialise{' '} - {' '} - SDKs. -

- - - Client-side Environment Key} - placeholder='Client-side Environment Key' - /> - - - -
-
-
- -
= ({ Identities
, )} - + + + + + SDK Keys + {environmentAdmin && ( @@ -91,6 +92,11 @@ export default ( exact component={EnvironmentSettingsPage} /> + Date: Wed, 8 May 2024 12:14:01 +0100 Subject: [PATCH 45/49] Add settings labels --- frontend/web/components/App.js | 4 ++-- frontend/web/components/pages/HomeAside.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index 6a1df5842092..27c9c3287998 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -628,7 +628,7 @@ const App = class extends Component { id='project-settings-link' to={`/project/${projectId}/settings`} > - Settings + Project Settings ) } @@ -675,7 +675,7 @@ const App = class extends Component { AccountStore.getOrganisation().id }/settings`} > - Settings + Organisation Settings )} diff --git a/frontend/web/components/pages/HomeAside.tsx b/frontend/web/components/pages/HomeAside.tsx index 6804ef5664f0..22d9cf1ea5e0 100644 --- a/frontend/web/components/pages/HomeAside.tsx +++ b/frontend/web/components/pages/HomeAside.tsx @@ -253,7 +253,7 @@ const HomeAside: FC = ({ fill='#9DA4AE' /> - Settings + Environment Settings )}
From ddffda4a7fdb134da36fe4c59f98574021131989 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 8 May 2024 12:18:08 +0100 Subject: [PATCH 46/49] Update settings icon, add settings labels --- frontend/web/components/pages/HomeAside.tsx | 6 ++---- frontend/web/components/svg/SettingsIcon.js | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/web/components/pages/HomeAside.tsx b/frontend/web/components/pages/HomeAside.tsx index 22d9cf1ea5e0..829fd2850a59 100644 --- a/frontend/web/components/pages/HomeAside.tsx +++ b/frontend/web/components/pages/HomeAside.tsx @@ -21,6 +21,7 @@ import Constants from 'common/constants' import EnvironmentSelect from 'components/EnvironmentSelect' import { components } from 'react-select' import Button from 'components/base/forms/Button' +import SettingsIcon from 'components/svg/SettingsIcon' type HomeAsideType = { environmentId: string @@ -248,10 +249,7 @@ const HomeAside: FC = ({ to={`/project/${project.id}/environment/${environment.api_key}/settings`} > - + Environment Settings diff --git a/frontend/web/components/svg/SettingsIcon.js b/frontend/web/components/svg/SettingsIcon.js index 0bbbfc71e240..46018e312c8a 100644 --- a/frontend/web/components/svg/SettingsIcon.js +++ b/frontend/web/components/svg/SettingsIcon.js @@ -1,6 +1,6 @@ import React from 'react' -function SettingsIcon({ className, fill, height, width }) { +function SettingsIcon({ fill = 'white', height = 24, width = 24 }) { return ( - + From cdac097c67fef78ac5906a253c73af661797b51f Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 8 May 2024 12:33:45 +0100 Subject: [PATCH 47/49] Tidy up sdk keys icon, fix change requests number not showing --- frontend/web/components/pages/HomeAside.tsx | 20 ++++++++----------- frontend/web/styles/project/_project-nav.scss | 8 ++++++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/web/components/pages/HomeAside.tsx b/frontend/web/components/pages/HomeAside.tsx index 829fd2850a59..2f777f06123e 100644 --- a/frontend/web/components/pages/HomeAside.tsx +++ b/frontend/web/components/pages/HomeAside.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react' +import React, { FC, useEffect } from 'react' import ProjectStore from 'common/stores/project-store' import ChangeRequestStore from 'common/stores/change-requests-store' import Utils from 'common/utils/utils' @@ -34,6 +34,11 @@ const HomeAside: FC = ({ history, projectId, }) => { + useEffect(() => { + if (environmentId) { + AppActions.getChangeRequests(environmentId, {}) + } + }, [environmentId]) const environment: Environment | null = environmentId === 'create' ? null @@ -181,7 +186,7 @@ const HomeAside: FC = ({ Change Requests{' '} {changeRequests ? ( - + {changeRequests} ) : null} @@ -230,16 +235,7 @@ const HomeAside: FC = ({ exact to={`/project/${project.id}/environment/${environment.api_key}/sdk-keys`} > - - - + SDK Keys {environmentAdmin && ( diff --git a/frontend/web/styles/project/_project-nav.scss b/frontend/web/styles/project/_project-nav.scss index a2a1f929c2f1..63286f588067 100644 --- a/frontend/web/styles/project/_project-nav.scss +++ b/frontend/web/styles/project/_project-nav.scss @@ -106,12 +106,14 @@ nav a { border-right: 1px solid $hr-border-color; } .collapsible__content { - svg { + svg, ion-icon { + text-align: center; width: 16px; height: 16px; + color: $body-color; + opacity: 0.6; path { fill: $body-color; - opacity: 0.6; } } ion-icon { @@ -137,6 +139,8 @@ nav a { padding-right: 4px; margin-right: 10px; line-height: 32px; + display: flex; + align-items: center; border-radius: $border-radius; &.active { background-color: $primary-alfa-8; From be764340219fdcb2c406ca85a54011207d861e6c Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Wed, 8 May 2024 13:07:54 +0100 Subject: [PATCH 48/49] Fix signup responsiveness --- frontend/web/components/pages/HomePage.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/web/components/pages/HomePage.js b/frontend/web/components/pages/HomePage.js index fb106edae4d9..d5cc948e86f9 100644 --- a/frontend/web/components/pages/HomePage.js +++ b/frontend/web/components/pages/HomePage.js @@ -162,7 +162,7 @@ const HomePage = class extends React.Component { const disableOauthRegister = Utils.getFlagsmithHasFeature( 'disable_oauth_registration', ) - const oauthClasses = 'col-12 col-md-4' + const oauthClasses = 'col-12 col-xl-4' if ((!isSignup || !disableOauthRegister) && !disableSignup) { if (Utils.getFlagsmithValue('oauth_github')) { @@ -170,7 +170,7 @@ const HomePage = class extends React.Component {
@@ -309,7 +310,9 @@ const HomePage = class extends React.Component { ) => ( <> {!!oauths.length && ( -
{oauths}
+
+ {oauths} +
)} {!preventEmailPassword && (
{!!oauths.length && ( -
{oauths}
+
+ {oauths} +
)} {!preventEmailPassword && ( From 4eb64ac9c5929fda9e2e45d25189ab1bb5b11483 Mon Sep 17 00:00:00 2001 From: kyle-ssg Date: Thu, 9 May 2024 09:27:02 +0100 Subject: [PATCH 49/49] Escape tag tooltips --- frontend/web/components/tags/TagContent.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/web/components/tags/TagContent.tsx b/frontend/web/components/tags/TagContent.tsx index 642a53376db5..14dc04db6f25 100644 --- a/frontend/web/components/tags/TagContent.tsx +++ b/frontend/web/components/tags/TagContent.tsx @@ -10,6 +10,12 @@ import OrganisationStore from 'common/stores/organisation-store' type TagContent = { tag: Partial } +function escapeHTML(unsafe: string) { + return unsafe.replace( + /[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g, + (c) => `&#${`000${c.charCodeAt(0)}`.slice(-4)};`, + ) +} const getTooltip = (tag: TTag | undefined) => { if (!tag) { @@ -45,7 +51,7 @@ const getTooltip = (tag: TTag | undefined) => { ).darken(0.1)};' class="chip d-inline-block chip--xs me-1" > - ${tag.label} + ${`${escapeHTML(tag.label)}`} ${tooltip || ''}
`