From ff0aed952c82125700a9b685c60b5a63166bd3fc Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 17 Jan 2023 15:12:35 +0100 Subject: [PATCH 01/11] enforce pricing plan for features (WIP) --- .../admin/organization/pricing_plan_utils.ts | 17 ++++++ .../components/pricing_enforcer.tsx | 54 +++++++++++++++++++ frontend/javascripts/navbar.tsx | 6 ++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 frontend/javascripts/components/pricing_enforcer.tsx diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index 635ae1b437d..3e9d78914bf 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -41,3 +41,20 @@ export function hasPricingPlanExceededStorage(organization: APIOrganization): bo export function isUserAllowedToRequestUpgrades(user: APIUser): boolean { return user.isAdmin || user.isOrganizationOwner; } + +export function isPricingPlanGreaterEqualThan(planA: PricingPlanEnum, planB: PricingPlanEnum) { + switch (planA) { + case PricingPlanEnum.Power: + case PricingPlanEnum.PowerTrial: + case PricingPlanEnum.Custom: + return true; + + case PricingPlanEnum.Team: + case PricingPlanEnum.TeamTrial: + return planB === PricingPlanEnum.Basic; + + case PricingPlanEnum.Basic: + default: + return false; + } +} diff --git a/frontend/javascripts/components/pricing_enforcer.tsx b/frontend/javascripts/components/pricing_enforcer.tsx new file mode 100644 index 00000000000..7ab3df9657a --- /dev/null +++ b/frontend/javascripts/components/pricing_enforcer.tsx @@ -0,0 +1,54 @@ +import { LockOutlined } from "@ant-design/icons"; +import { PricingPlanEnum } from "admin/organization/organization_edit_view"; +import { isPricingPlanGreaterEqualThan } from "admin/organization/pricing_plan_utils"; +import { Tooltip, Menu, MenuItemProps } from "antd"; +import { MenuClickEventHandler } from "rc-menu/lib/interface"; +import * as React from "react"; + +export default function PricingEnforcedMenuItem({ + children, + requiredPricingPlan, + showLockIcon = true, + ...args +}: { + children: JSX.Element; + requiredPricingPlan: PricingPlanEnum; + showLockIcon?: boolean; +} & MenuItemProps): JSX.Element { + const isFeatureAllowed = false; + // const isFeatureAllowed = isPricingPlanGreaterThan(organization.pricingPlan, requiredPricingPlan); + + if (isFeatureAllowed) return children; + + // TODO show upragde button for owner + const toolTipMessage = `This feature is not available in your organisation's plan. Ask your organisation owner to upgrade.`; + + const handleMouseClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + const handleMenuClick: MenuClickEventHandler = (info) => { + info.domEvent.preventDefault(); + info.domEvent.stopPropagation(); + }; + + // onClick = { handleMouseClick }; + // onAuxClick = { handleMouseClick }; + // onDoubleClick = { handleMouseClick }; + // onClickCapture = { handleMouseClick }; + + return ( + + : null} + onClick={handleMenuClick} + onAuxClick={handleMouseClick} + onDoubleClick={handleMouseClick} + onClickCapture={handleMouseClick} + > + {children} + + + ); +} diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 98588bfb6f5..0529f418e47 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -37,6 +37,8 @@ import window, { document, location } from "libs/window"; import features from "features"; import { setThemeAction } from "oxalis/model/actions/ui_actions"; import { HelpModal } from "oxalis/view/help_modal"; +import { PricingPlanEnum } from "admin/organization/organization_edit_view"; +import PricingEnforcedMenuItem from "components/pricing_enforcer"; const { SubMenu } = Menu; const { Header } = Layout; @@ -213,9 +215,9 @@ function AdministrationSubMenu({ Teams - + Projects - + Tasks From 5b404779b0d29a9adb88a2025fa9939bf97c5ec2 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 18 Jan 2023 09:52:46 +0100 Subject: [PATCH 02/11] enforce pricing for navbar links --- .../components/pricing_enforcer.tsx | 28 ++++++++--------- frontend/javascripts/navbar.tsx | 30 +++++++++++-------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/frontend/javascripts/components/pricing_enforcer.tsx b/frontend/javascripts/components/pricing_enforcer.tsx index 7ab3df9657a..0f3077045db 100644 --- a/frontend/javascripts/components/pricing_enforcer.tsx +++ b/frontend/javascripts/components/pricing_enforcer.tsx @@ -9,7 +9,7 @@ export default function PricingEnforcedMenuItem({ children, requiredPricingPlan, showLockIcon = true, - ...args + ...menuItemProps }: { children: JSX.Element; requiredPricingPlan: PricingPlanEnum; @@ -33,22 +33,18 @@ export default function PricingEnforcedMenuItem({ info.domEvent.stopPropagation(); }; - // onClick = { handleMouseClick }; - // onAuxClick = { handleMouseClick }; - // onDoubleClick = { handleMouseClick }; - // onClickCapture = { handleMouseClick }; - return ( - - : null} - onClick={handleMenuClick} - onAuxClick={handleMouseClick} - onDoubleClick={handleMouseClick} - onClickCapture={handleMouseClick} - > - {children} - + + : null} + onClick={handleMenuClick} + onAuxClick={handleMouseClick} + onDoubleClick={handleMouseClick} + onClickCapture={handleMouseClick} + {...menuItemProps} + > + {children} + ); } diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 0529f418e47..54a664f498e 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -218,12 +218,12 @@ function AdministrationSubMenu({ Projects - + Tasks - - + + Task Types - + {features().jobsEnabled && ( Processing Jobs @@ -256,18 +256,24 @@ function StatisticsSubMenu({ collapse, ...menuProps }: { collapse: boolean } & S } {...menuProps} > - + Overview - - + + Time Tracking - - + + Project Progress - - + + Open Tasks - + ); } From 1c87eb6d9caaecb32c7e50b24c6f3bff49992ca6 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 19 Jan 2023 11:55:14 +0100 Subject: [PATCH 03/11] add organization to store --- .../organization/organization_edit_view.tsx | 58 ++++++++++--------- .../javascripts/admin/user/user_list_view.tsx | 19 +++--- .../components/pricing_enforcer.tsx | 2 +- .../javascripts/components/secured_route.tsx | 47 +++++++++++---- .../javascripts/dashboard/dashboard_view.tsx | 17 +++--- frontend/javascripts/main.tsx | 14 ++++- frontend/javascripts/navbar.tsx | 8 +-- frontend/javascripts/oxalis/default_state.ts | 1 + .../oxalis/model/actions/actions.ts | 8 ++- .../model/actions/organization_actions.ts | 11 ++++ .../model/reducers/organization_reducer.ts | 20 +++++++ frontend/javascripts/oxalis/store.ts | 4 ++ frontend/javascripts/router.tsx | 31 ++++++++-- 13 files changed, 169 insertions(+), 71 deletions(-) create mode 100644 frontend/javascripts/oxalis/model/actions/organization_actions.ts create mode 100644 frontend/javascripts/oxalis/model/reducers/organization_reducer.ts diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx index ff9d50ca6df..bf2e62bdc8b 100644 --- a/frontend/javascripts/admin/organization/organization_edit_view.tsx +++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx @@ -1,4 +1,5 @@ -import { RouteComponentProps, withRouter } from "react-router-dom"; +import React from "react"; +import { connect } from "react-redux"; import { Form, Button, Card, Input, Row, FormInstance, Col, Skeleton } from "antd"; import { MailOutlined, @@ -7,10 +8,8 @@ import { SaveOutlined, IdcardOutlined, } from "@ant-design/icons"; -import React from "react"; import { confirmAsync } from "dashboard/dataset/helper_components"; import { - getOrganization, deleteOrganization, updateOrganization, getUsers, @@ -27,6 +26,7 @@ import { PlanUpgradeCard, } from "./organization_cards"; import { getActiveUserCount } from "./pricing_plan_utils"; +import type { OxalisState } from "oxalis/store"; const FormItem = Form.Item; export enum PricingPlanEnum { @@ -37,31 +37,33 @@ export enum PricingPlanEnum { PowerTrial = "Power_Trial", Custom = "Custom", } -type Props = { - organizationName: string; + +type StateProps = { + organization: APIOrganization; }; +type Props = StateProps; + type State = { displayName: string; newUserMailingList: string; pricingPlan: PricingPlanEnum | null | undefined; isFetchingData: boolean; isDeleting: boolean; - organization: APIOrganization | null; activeUsersCount: number; pricingPlanStatus: APIPricingPlanStatus | null; }; class OrganizationEditView extends React.PureComponent { state: State = { - displayName: "", - newUserMailingList: "", - pricingPlan: null, + displayName: this.props.organization.displayName, + newUserMailingList: this.props.organization.newUserMailingList, + pricingPlan: this.props.organization.pricingPlan, isFetchingData: false, isDeleting: false, - organization: null, activeUsersCount: 1, pricingPlanStatus: null, }; + formRef = React.createRef(); componentDidMount() { @@ -98,19 +100,14 @@ class OrganizationEditView extends React.PureComponent { this.setState({ isFetchingData: true, }); - const [organization, users, pricingPlanStatus] = await Promise.all([ - getOrganization(this.props.organizationName), - getUsers(), - getPricingPlanStatus(), - ]); + const [users, pricingPlanStatus] = await Promise.all([getUsers(), getPricingPlanStatus()]); - const { displayName, newUserMailingList, pricingPlan } = organization; + const { displayName, newUserMailingList, pricingPlan } = this.props.organization; this.setState({ displayName, pricingPlan: coalesce(PricingPlanEnum, pricingPlan), newUserMailingList, isFetchingData: false, - organization, pricingPlanStatus, activeUsersCount: getActiveUserCount(users), }); @@ -119,7 +116,7 @@ class OrganizationEditView extends React.PureComponent { // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'formValues' implicitly has an 'any' typ... Remove this comment to see the full error message onFinish = async (formValues) => { await updateOrganization( - this.props.organizationName, + this.props.organization.name, formValues.displayName, formValues.newUserMailingList, ); @@ -142,7 +139,7 @@ class OrganizationEditView extends React.PureComponent { this.setState({ isDeleting: true, }); - await deleteOrganization(this.props.organizationName); + await deleteOrganization(this.props.organization.name); this.setState({ isDeleting: false, }); @@ -151,14 +148,14 @@ class OrganizationEditView extends React.PureComponent { }; handleCopyNameButtonClicked = async (): Promise => { - await navigator.clipboard.writeText(this.props.organizationName); + await navigator.clipboard.writeText(this.props.organization.name); Toast.success("Copied organization name to the clipboard."); }; render() { if ( this.state.isFetchingData || - !this.state.organization || + !this.props.organization || !this.state.pricingPlan || !this.state.pricingPlanStatus ) @@ -191,18 +188,18 @@ class OrganizationEditView extends React.PureComponent {

{this.state.displayName}

{this.state.pricingPlanStatus.isExceeded ? ( - + ) : null} {this.state.pricingPlanStatus.isAlmostExceeded && !this.state.pricingPlanStatus.isExceeded ? ( - + ) : null} - - + +
{ } - value={this.props.organizationName} + value={this.props.organization.name} style={{ width: "calc(100% - 31px)", }} @@ -303,4 +300,9 @@ class OrganizationEditView extends React.PureComponent { } } -export default withRouter(OrganizationEditView); +const mapStateToProps = (state: OxalisState): StateProps => ({ + organization: state.activeOrganization, +}); + +const connector = connect(mapStateToProps); +export default connector(OrganizationEditView); diff --git a/frontend/javascripts/admin/user/user_list_view.tsx b/frontend/javascripts/admin/user/user_list_view.tsx index 56ffa0d0879..aec701a67f9 100644 --- a/frontend/javascripts/admin/user/user_list_view.tsx +++ b/frontend/javascripts/admin/user/user_list_view.tsx @@ -16,11 +16,16 @@ import { UserOutlined, } from "@ant-design/icons"; import { connect } from "react-redux"; -import * as React from "react"; +import React from "react"; import _ from "lodash"; import moment from "moment"; import { location } from "libs/window"; -import type { APIUser, APITeamMembership, ExperienceMap } from "types/api_flow_types"; +import type { + APIUser, + APITeamMembership, + ExperienceMap, + APIOrganization, +} from "types/api_flow_types"; import { InviteUsersModal } from "admin/onboarding"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; @@ -44,6 +49,7 @@ const typeHint: APIUser[] = []; type StateProps = { activeUser: APIUser; + activeOrganization: APIOrganization; }; type Props = RouteComponentProps & StateProps; @@ -80,7 +86,7 @@ class UserListView extends React.PureComponent { searchQuery: "", singleSelectedUser: null, domainToEdit: null, - maxUserCountPerOrganization: Number.POSITIVE_INFINITY, + maxUserCountPerOrganization: this.props.activeOrganization.includedUsers, }; componentDidMount() { @@ -103,15 +109,11 @@ class UserListView extends React.PureComponent { this.setState({ isLoading: true, }); - const [users, organization] = await Promise.all([ - getEditableUsers(), - getOrganization(this.props.activeUser.organization), - ]); + const users = await getEditableUsers(); this.setState({ isLoading: false, users, - maxUserCountPerOrganization: organization.includedUsers, }); } @@ -681,6 +683,7 @@ class UserListView extends React.PureComponent { const mapStateToProps = (state: OxalisState): StateProps => ({ activeUser: enforceActiveUser(state.activeUser), + activeOrganization: state.activeOrganization, }); const connector = connect(mapStateToProps); diff --git a/frontend/javascripts/components/pricing_enforcer.tsx b/frontend/javascripts/components/pricing_enforcer.tsx index 0f3077045db..f98b75224db 100644 --- a/frontend/javascripts/components/pricing_enforcer.tsx +++ b/frontend/javascripts/components/pricing_enforcer.tsx @@ -36,7 +36,6 @@ export default function PricingEnforcedMenuItem({ return ( : null} onClick={handleMenuClick} onAuxClick={handleMouseClick} onDoubleClick={handleMouseClick} @@ -44,6 +43,7 @@ export default function PricingEnforcedMenuItem({ {...menuItemProps} > {children} + {showLockIcon ? : null} ); diff --git a/frontend/javascripts/components/secured_route.tsx b/frontend/javascripts/components/secured_route.tsx index 3961b6cccb0..735689605ce 100644 --- a/frontend/javascripts/components/secured_route.tsx +++ b/frontend/javascripts/components/secured_route.tsx @@ -1,17 +1,27 @@ -import type { RouteComponentProps } from "react-router-dom"; -import { Route, withRouter } from "react-router-dom"; -import type { ComponentType } from "react"; import React from "react"; +import { Route, withRouter } from "react-router-dom"; +import { connect } from "react-redux"; import LoginView from "admin/auth/login_view"; +import { PricingPlanEnum } from "admin/organization/organization_edit_view"; +import { isPricingPlanGreaterEqualThan } from "admin/organization/pricing_plan_utils"; +import type { ComponentType } from "react"; +import type { RouteComponentProps } from "react-router-dom"; +import type { OxalisState } from "oxalis/store"; +import { APIOrganization } from "types/api_flow_types"; -export type SecuredRouteProps = RouteComponentProps & { - component?: ComponentType; - path: string; - render?: (arg0: RouteComponentProps) => React.ReactNode; - isAuthenticated: boolean; - serverAuthenticationCallback?: (...args: Array) => any; - exact?: boolean; +type StateProps = { + activeOrganization: APIOrganization; }; +export type SecuredRouteProps = RouteComponentProps & + StateProps & { + component?: ComponentType; + path: string; + render?: (arg0: RouteComponentProps) => React.ReactNode; + isAuthenticated: boolean; + requiredPricingPlan?: PricingPlanEnum; + serverAuthenticationCallback?: (...args: Array) => any; + exact?: boolean; + }; type State = { isAdditionallyAuthenticated: boolean; }; @@ -63,11 +73,26 @@ class SecuredRoute extends React.PureComponent { } } + if ( + this.props.requiredPricingPlan && + isPricingPlanGreaterEqualThan( + this.props.activeOrganization.pricingPlan, + this.props.requiredPricingPlan, + ) + ) { + return ; + } + return ; }} /> ); } } +const mapStateToProps = (state: OxalisState): StateProps => ({ + activeOrganization: state.activeOrganization, +}); + +const connector = connect(mapStateToProps); -export default withRouter(SecuredRoute); +export default connector(withRouter(SecuredRoute)); diff --git a/frontend/javascripts/dashboard/dashboard_view.tsx b/frontend/javascripts/dashboard/dashboard_view.tsx index efe38ae16cc..991ba1f76c6 100644 --- a/frontend/javascripts/dashboard/dashboard_view.tsx +++ b/frontend/javascripts/dashboard/dashboard_view.tsx @@ -11,7 +11,6 @@ import type { APIOrganization, APIPricingPlanStatus, APIUser } from "types/api_f import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; import { - getOrganization, getPricingPlanStatus, getUser, updateNovelUserExperienceInfos, @@ -39,6 +38,7 @@ type OwnProps = { }; type StateProps = { activeUser: APIUser; + activeOrganization: APIOrganization; }; type DispatchProps = { updateActiveUser: (arg0: APIUser) => void; @@ -124,14 +124,10 @@ class DashboardView extends PureComponent { const user = this.props.userId != null ? await getUser(this.props.userId) : this.props.activeUser; - const [organization, pricingPlanStatus] = await Promise.all([ - getOrganization(user.organization), - getPricingPlanStatus(), - ]); + const pricingPlanStatus = await getPricingPlanStatus(); this.setState({ user, - organization, pricingPlanStatus, }); } @@ -287,14 +283,14 @@ class DashboardView extends PureComponent { // ToDo enable components below once pricing goes live const pricingPlanWarnings = - this.state.organization && + this.props.activeOrganization && this.state.pricingPlanStatus?.isAlmostExceeded && !this.state.pricingPlanStatus.isExceeded ? ( - + ) : null; const pricingPlanErrors = - this.state.organization && this.state.pricingPlanStatus?.isExceeded ? ( - + this.props.activeOrganization && this.state.pricingPlanStatus?.isExceeded ? ( + ) : null; return ( @@ -334,6 +330,7 @@ function DatasetViewWithLegacyContext({ user }: { user: APIUser }) { const mapStateToProps = (state: OxalisState): StateProps => ({ activeUser: enforceActiveUser(state.activeUser), + activeOrganization: state.activeOrganization, }); const mapDispatchToProps = (dispatch: Dispatch) => ({ diff --git a/frontend/javascripts/main.tsx b/frontend/javascripts/main.tsx index f8c7cc1372f..e740f4d28d1 100644 --- a/frontend/javascripts/main.tsx +++ b/frontend/javascripts/main.tsx @@ -5,7 +5,7 @@ import { document } from "libs/window"; import rootSaga from "oxalis/model/sagas/root_saga"; import UnthrottledStore, { startSagas } from "oxalis/store"; -import { getActiveUser, checkAnyOrganizationExists } from "admin/admin_rest_api"; +import { getActiveUser, checkAnyOrganizationExists, getOrganization } from "admin/admin_rest_api"; import { googleAnalyticsLogClicks } from "oxalis/model/helpers/analytics"; import { load as loadFeatureToggles } from "features"; import { setActiveUserAction } from "oxalis/model/actions/user_actions"; @@ -24,6 +24,7 @@ import ErrorBoundary from "components/error_boundary"; import { setStore, setModel } from "oxalis/singletons"; import Model from "oxalis/model"; import { setupApi } from "oxalis/api/internal_api"; +import { setActiveOrganizationAction } from "oxalis/model/actions/organization_actions"; setModel(Model); setStore(UnthrottledStore); @@ -71,12 +72,23 @@ async function loadHasOrganizations() { } } +async function loadOrganization() { + try { + const { activeUser } = Store.getState(); + const organization = await getOrganization(activeUser?.organization); + Store.dispatch(setActiveOrganizationAction(organization)); + } catch (e) { + // pass + } +} + document.addEventListener("DOMContentLoaded", async () => { ErrorHandling.initialize({ throwAssertions: false, }); document.addEventListener("click", googleAnalyticsLogClicks); await Promise.all([loadFeatureToggles(), loadActiveUser(), loadHasOrganizations()]); + await Promise.all([loadOrganization()]); const containerElement = document.getElementById("main-container"); if (containerElement) { diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 54a664f498e..4fc22662c4c 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -256,9 +256,9 @@ function StatisticsSubMenu({ collapse, ...menuProps }: { collapse: boolean } & S } {...menuProps} > - + Overview - + Project Progress - + Open Tasks diff --git a/frontend/javascripts/oxalis/default_state.ts b/frontend/javascripts/oxalis/default_state.ts index aef0e7a3575..5e5d7389f4a 100644 --- a/frontend/javascripts/oxalis/default_state.ts +++ b/frontend/javascripts/oxalis/default_state.ts @@ -214,6 +214,7 @@ const defaultState: OxalisState = { }, }, activeUser: null, + activeOrganization: null, uiInformation: { activeTool: "MOVE", showDropzoneModal: false, diff --git a/frontend/javascripts/oxalis/model/actions/actions.ts b/frontend/javascripts/oxalis/model/actions/actions.ts index 67073ef3161..05bc321d1cb 100644 --- a/frontend/javascripts/oxalis/model/actions/actions.ts +++ b/frontend/javascripts/oxalis/model/actions/actions.ts @@ -11,7 +11,9 @@ import type { UserAction } from "oxalis/model/actions/user_actions"; import type { ViewModeAction } from "oxalis/model/actions/view_mode_actions"; import type { VolumeTracingAction } from "oxalis/model/actions/volumetracing_actions"; import type { ConnectomeAction } from "oxalis/model/actions/connectome_actions"; -import { ProofreadAction } from "oxalis/model/actions//proofread_actions"; +import { ProofreadAction } from "oxalis/model/actions/proofread_actions"; +import { OrganizationAction } from "oxalis/model/actions/organization_actions"; + export type Action = | SkeletonTracingAction | VolumeTracingAction @@ -26,7 +28,9 @@ export type Action = | UiAction | SegmentationAction | ConnectomeAction - | ProofreadAction; + | ProofreadAction + | OrganizationAction; + export const wkReadyAction = () => ({ type: "WK_READY", }); diff --git a/frontend/javascripts/oxalis/model/actions/organization_actions.ts b/frontend/javascripts/oxalis/model/actions/organization_actions.ts new file mode 100644 index 00000000000..02cad86aafd --- /dev/null +++ b/frontend/javascripts/oxalis/model/actions/organization_actions.ts @@ -0,0 +1,11 @@ +import type { APIOrganization } from "types/api_flow_types"; + +type SetActiveOrganization = ReturnType; + +export type OrganizationAction = SetActiveOrganization; + +export const setActiveOrganizationAction = (organization: APIOrganization) => + ({ + type: "SET_ACTIVE_ORGANIZATION", + organization, + } as const); diff --git a/frontend/javascripts/oxalis/model/reducers/organization_reducer.ts b/frontend/javascripts/oxalis/model/reducers/organization_reducer.ts new file mode 100644 index 00000000000..18c30da7a0d --- /dev/null +++ b/frontend/javascripts/oxalis/model/reducers/organization_reducer.ts @@ -0,0 +1,20 @@ +import update from "immutability-helper"; +import type { Action } from "oxalis/model/actions/actions"; +import type { OxalisState } from "oxalis/store"; + +function OrganizationReducer(state: OxalisState, action: Action): OxalisState { + switch (action.type) { + case "SET_ACTIVE_ORGANIZATION": { + return update(state, { + activeOrganization: { + $set: action.organization, + }, + }); + } + + default: + return state; + } +} + +export default OrganizationReducer; diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index afeeac6ed5b..feeef559fb0 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -24,6 +24,7 @@ import type { TracingType, APIMeshFile, ServerEditableMapping, + APIOrganization, } from "types/api_flow_types"; import type { Action } from "oxalis/model/actions/actions"; import type { @@ -66,6 +67,7 @@ import overwriteActionMiddleware from "oxalis/model/helpers/overwrite_action_mid import reduceReducers from "oxalis/model/helpers/reduce_reducers"; import ConnectomeReducer from "oxalis/model/reducers/connectome_reducer"; import { SaveQueueType } from "./model/actions/save_actions"; +import OrganizationReducer from "./model/reducers/organization_reducer"; export type MutableCommentType = { content: string; @@ -518,6 +520,7 @@ export type OxalisState = { readonly flycam: Flycam; readonly viewModeData: ViewModeData; readonly activeUser: APIUser | null | undefined; + readonly activeOrganization: APIOrganization | null; readonly uiInformation: UiInformation; readonly localSegmentationData: Record< string, @@ -550,6 +553,7 @@ const combinedReducers = reduceReducers( UserReducer, UiReducer, ConnectomeReducer, + OrganizationReducer, ); const store = createStore( diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index c63c16dcf29..121c1a33898 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -14,7 +14,7 @@ import StartResetPasswordView from "admin/auth/start_reset_password_view"; import DatasetAddView from "admin/dataset/dataset_add_view"; import JobListView from "admin/job/job_list_view"; import Onboarding from "admin/onboarding"; -import OrganizationEditView from "admin/organization/organization_edit_view"; +import OrganizationEditView, { PricingPlanEnum } from "admin/organization/organization_edit_view"; import ProjectCreateView from "admin/project/project_create_view"; import ProjectListView from "admin/project/project_list_view"; import ScriptCreateView from "admin/scripts/script_create_view"; @@ -57,7 +57,12 @@ import { connect } from "react-redux"; // @ts-expect-error ts-migrate(2305) FIXME: Module '"react-router-dom"' has no exported member... Remove this comment to see the full error message import type { ContextRouter, RouteProps } from "react-router-dom"; import { Redirect, Route, Router, Switch } from "react-router-dom"; -import { APICompoundTypeEnum, APIUser, TracingTypeEnum } from "types/api_flow_types"; +import { + APICompoundTypeEnum, + APIPricingPlanStatus, + APIUser, + TracingTypeEnum, +} from "types/api_flow_types"; import ErrorBoundary from "components/error_boundary"; @@ -80,6 +85,7 @@ const AsyncWorkflowListView = loadable(() => import("admin/voxelytics/workflow_l type StateProps = { activeUser: APIUser | null | undefined; hasOrganizations: boolean; + pricingPlan: PricingPlanEnum; }; type Props = StateProps; const browserHistory = createBrowserHistory(); @@ -300,12 +306,14 @@ class ReactRouter extends React.Component { /> { ( )} @@ -331,6 +342,7 @@ class ReactRouter extends React.Component { ( { } @@ -350,11 +363,13 @@ class ReactRouter extends React.Component { } /> ( { ( )} @@ -442,11 +458,13 @@ class ReactRouter extends React.Component { ( )} @@ -454,6 +472,7 @@ class ReactRouter extends React.Component { ( { ( )} @@ -495,10 +515,7 @@ class ReactRouter extends React.Component { ( - // @ts-expect-error ts-migrate(2339) FIXME: Property 'organizationName' does not exist on type... Remove this comment to see the full error message - - )} + render={() => } /> { } /> { const mapStateToProps = (state: OxalisState): StateProps => ({ activeUser: state.activeUser, + pricingPlan: state.activeOrganization.pricingPlan, hasOrganizations: state.uiInformation.hasOrganizations, }); From 5c2e5581a919e4c0b57e39be6edf37dbe56a0f04 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 19 Jan 2023 13:51:25 +0100 Subject: [PATCH 04/11] fix type errors for activeOrganization --- .../javascripts/admin/user/user_list_view.tsx | 49 ++++++++++--------- .../javascripts/components/secured_route.tsx | 25 +++++----- frontend/javascripts/main.tsx | 8 ++- frontend/javascripts/messages.tsx | 1 + .../model/accessors/organization_accessors.ts | 12 +++++ frontend/javascripts/router.tsx | 4 +- 6 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 frontend/javascripts/oxalis/model/accessors/organization_accessors.ts diff --git a/frontend/javascripts/admin/user/user_list_view.tsx b/frontend/javascripts/admin/user/user_list_view.tsx index aec701a67f9..44fe870895f 100644 --- a/frontend/javascripts/admin/user/user_list_view.tsx +++ b/frontend/javascripts/admin/user/user_list_view.tsx @@ -42,6 +42,7 @@ import * as Utils from "libs/utils"; import messages from "messages"; import { logoutUserAction } from "../../oxalis/model/actions/user_actions"; import Store from "../../oxalis/store"; +import { enforceActiveOrganization } from "oxalis/model/accessors/organization_accessors"; const { Column } = Table; const { Search } = Input; @@ -57,9 +58,9 @@ type State = { isLoading: boolean; users: Array; selectedUserIds: Key[]; - isExperienceModalVisible: boolean; - isTeamRoleModalVisible: boolean; - isInviteModalVisible: boolean; + isExperienceModalOpen: boolean; + isTeamRoleModalOpen: boolean; + isInviteModalOpen: boolean; singleSelectedUser: APIUser | null | undefined; activationFilter: Array<"true" | "false">; searchQuery: string; @@ -79,9 +80,9 @@ class UserListView extends React.PureComponent { isLoading: true, users: [], selectedUserIds: [], - isExperienceModalVisible: false, - isTeamRoleModalVisible: false, - isInviteModalVisible: false, + isExperienceModalOpen: false, + isTeamRoleModalOpen: false, + isInviteModalOpen: false, activationFilter: ["true"], searchQuery: "", singleSelectedUser: null, @@ -96,7 +97,7 @@ class UserListView extends React.PureComponent { if (location.hash === "#invite") { this.setState({ - isInviteModalVisible: true, + isInviteModalOpen: true, }); } } @@ -133,7 +134,7 @@ class UserListView extends React.PureComponent { this.setState({ users: newUsers, selectedUserIds: [selectedUser.id], - isTeamRoleModalVisible: isActive, + isTeamRoleModalOpen: isActive, }); }, () => {}, // Do nothing, change did not succeed @@ -171,8 +172,8 @@ class UserListView extends React.PureComponent { handleUsersChange = (updatedUsers: Array): void => { this.setState({ users: updatedUsers, - isExperienceModalVisible: false, - isTeamRoleModalVisible: false, + isExperienceModalOpen: false, + isTeamRoleModalOpen: false, }); }; @@ -180,7 +181,7 @@ class UserListView extends React.PureComponent { const updatedUsersMap = _.keyBy(updatedUsers, (u) => u.id); this.setState((prevState) => ({ - isExperienceModalVisible: false, + isExperienceModalOpen: false, users: prevState.users.map((user) => updatedUsersMap[user.id] || user), singleSelectedUser: null, selectedUserIds: prevState.singleSelectedUser == null ? [] : prevState.selectedUserIds, @@ -248,7 +249,7 @@ class UserListView extends React.PureComponent { renderInviteUsersAlert() { const inviteUsersCallback = () => this.setState({ - isInviteModalVisible: true, + isInviteModalOpen: true, }); const noUsersMessage = ( @@ -361,7 +362,7 @@ class UserListView extends React.PureComponent { ; + + return ( + + + + ); +} + export function PageUnavailableForYourPlanView() { const organizationName = useSelector((state: OxalisState) => state.activeOrganization?.name); diff --git a/frontend/javascripts/dashboard/dataset_view.tsx b/frontend/javascripts/dashboard/dataset_view.tsx index 98b30ab31db..d3fad9b2d77 100644 --- a/frontend/javascripts/dashboard/dataset_view.tsx +++ b/frontend/javascripts/dashboard/dataset_view.tsx @@ -48,6 +48,8 @@ import { SEARCH_RESULTS_LIMIT, useFolderQuery, } from "./dataset/queries"; +import { PricingEnforcedButton } from "components/pricing_enforcers"; +import { PricingPlanEnum } from "admin/organization/organization_edit_view"; const { Group: InputGroup } = Input; @@ -291,7 +293,7 @@ function DatasetView(props: Props) { {"activeFolderId" in context && context.activeFolderId != null && ( - + )} {search} diff --git a/frontend/javascripts/dashboard/folders/folder_tree.tsx b/frontend/javascripts/dashboard/folders/folder_tree.tsx index 69730a45aee..0e8688d2e12 100644 --- a/frontend/javascripts/dashboard/folders/folder_tree.tsx +++ b/frontend/javascripts/dashboard/folders/folder_tree.tsx @@ -15,6 +15,8 @@ import { Key } from "antd/lib/table/interface"; import memoizeOne from "memoize-one"; import classNames from "classnames"; import { FolderItem } from "types/api_flow_types"; +import { PricingEnforcedMenuItem } from "components/pricing_enforcers"; +import { PricingPlanEnum } from "admin/organization/organization_edit_view"; const { DirectoryTree } = Tree; @@ -143,7 +145,9 @@ export function FolderTreeSidebar({ const createMenu = () => ( - Please right-click an existing folder. + + Please right-click an existing folder. + ); @@ -219,19 +223,26 @@ function generateTitle( const createMenu = () => ( - context.showCreateFolderPrompt(id)} disabled={!folder.isEditable} + requiredPricingPlan={PricingPlanEnum.Team} > New Folder - - + + Edit Folder - + Date: Fri, 20 Jan 2023 14:52:24 +0100 Subject: [PATCH 08/11] applied PR feedback --- .../admin/organization/pricing_plan_utils.ts | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index 1a9675846ba..a21367a476d 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -42,26 +42,18 @@ export function isUserAllowedToRequestUpgrades(user: APIUser): boolean { return user.isAdmin || user.isOrganizationOwner; } -export function isPricingPlanGreaterEqualThan(planA: PricingPlanEnum, planB: PricingPlanEnum) { - switch (planA) { - case PricingPlanEnum.Power: - case PricingPlanEnum.PowerTrial: - case PricingPlanEnum.Custom: - return true; - - case PricingPlanEnum.Team: - case PricingPlanEnum.TeamTrial: - switch (planB) { - case PricingPlanEnum.Power: - case PricingPlanEnum.PowerTrial: - case PricingPlanEnum.Custom: - return false; - default: - return true; - } - - case PricingPlanEnum.Basic: - default: - return false; - } +const PLAN_TO_RANK: Map = new Map([ + [PricingPlanEnum.Basic, 0], + [PricingPlanEnum.Team, 1], + [PricingPlanEnum.TeamTrial, 1], + [PricingPlanEnum.Power, 2], + [PricingPlanEnum.PowerTrial, 2], + [PricingPlanEnum.Custom, 2], +]); + +export function isPricingPlanGreaterEqualThan( + planA: PricingPlanEnum, + planB: PricingPlanEnum, +): boolean { + return PLAN_TO_RANK.get(planA) >= PLAN_TO_RANK.get(planB); } From 04d2f1358d13cfd2cf7016a515701a5585f27665 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 20 Jan 2023 16:15:49 +0100 Subject: [PATCH 09/11] applied even more PR feedback --- .../admin/dataset/dataset_upload_view.tsx | 8 +-- .../organization/organization_edit_view.tsx | 57 +++---------------- .../admin/organization/pricing_plan_utils.ts | 18 +++--- .../javascripts/admin/user/user_list_view.tsx | 8 +-- .../components/pricing_enforcers.tsx | 27 ++++++--- .../javascripts/components/secured_route.tsx | 11 ++-- .../javascripts/dashboard/dashboard_view.tsx | 1 - frontend/javascripts/main.tsx | 16 ++---- frontend/javascripts/messages.tsx | 2 +- .../model/accessors/organization_accessors.ts | 2 +- .../dataset_info_tab_view.tsx | 4 +- 11 files changed, 56 insertions(+), 98 deletions(-) diff --git a/frontend/javascripts/admin/dataset/dataset_upload_view.tsx b/frontend/javascripts/admin/dataset/dataset_upload_view.tsx index 61ffc5637c8..6ed0c4fed60 100644 --- a/frontend/javascripts/admin/dataset/dataset_upload_view.tsx +++ b/frontend/javascripts/admin/dataset/dataset_upload_view.tsx @@ -690,20 +690,20 @@ class DatasetUploadView extends React.Component { return; } - const teamOfOrganisation = fetchedTeams.find( + const teamOfOrganization = fetchedTeams.find( (team) => team.name === team.organization, ); - if (teamOfOrganisation == null) { + if (teamOfOrganization == null) { return; } if (this.formRef.current == null) return; this.formRef.current.setFieldsValue({ - initialTeams: [teamOfOrganisation], + initialTeams: [teamOfOrganization], }); this.setState({ - selectedTeams: [teamOfOrganisation], + selectedTeams: [teamOfOrganization], }); }} /> diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx index b5d74f8cf9a..96868e33bba 100644 --- a/frontend/javascripts/admin/organization/organization_edit_view.tsx +++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx @@ -16,7 +16,6 @@ import { getPricingPlanStatus, } from "admin/admin_rest_api"; import Toast from "libs/toast"; -import { coalesce } from "libs/utils"; import { APIOrganization, APIPricingPlanStatus } from "types/api_flow_types"; import { PlanAboutToExceedAlert, @@ -39,15 +38,11 @@ export enum PricingPlanEnum { Custom = "Custom", } -type StateProps = { +type Props = { organization: APIOrganization; }; -type Props = StateProps; type State = { - displayName: string; - newUserMailingList: string; - pricingPlan: PricingPlanEnum | null | undefined; isFetchingData: boolean; isDeleting: boolean; activeUsersCount: number; @@ -56,9 +51,6 @@ type State = { class OrganizationEditView extends React.PureComponent { state: State = { - displayName: this.props.organization.displayName, - newUserMailingList: this.props.organization.newUserMailingList, - pricingPlan: this.props.organization.pricingPlan, isFetchingData: false, isDeleting: false, activeUsersCount: 1, @@ -71,43 +63,13 @@ class OrganizationEditView extends React.PureComponent { this.fetchData(); } - componentDidUpdate(_prevProps: Props, prevState: State) { - if (this.formRef.current != null) { - // initialValues only works on the first render. Afterwards, values need to be updated - // using setFieldsValue - if ( - prevState.displayName.length === 0 && - this.state.displayName.length > 0 && - this.formRef.current.getFieldValue("displayName") !== this.state.displayName - ) { - this.formRef.current.setFieldsValue({ - displayName: this.state.displayName, - }); - } - - if ( - prevState.newUserMailingList.length === 0 && - this.state.newUserMailingList.length > 0 && - this.formRef.current.getFieldValue("newUserMailingList") !== this.state.newUserMailingList - ) { - this.formRef.current.setFieldsValue({ - newUserMailingList: this.state.newUserMailingList, - }); - } - } - } - async fetchData() { this.setState({ isFetchingData: true, }); const [users, pricingPlanStatus] = await Promise.all([getUsers(), getPricingPlanStatus()]); - const { displayName, newUserMailingList, pricingPlan } = this.props.organization; this.setState({ - displayName, - pricingPlan: coalesce(PricingPlanEnum, pricingPlan), - newUserMailingList, isFetchingData: false, pricingPlanStatus, activeUsersCount: getActiveUserCount(users), @@ -129,7 +91,7 @@ class OrganizationEditView extends React.PureComponent { title: (

Deleting an organization cannot be undone. Are you certain you want to delete the - organization {this.state.displayName}?
+ organization {this.props.organization.displayName}?
Attention: You will be logged out.

), @@ -154,12 +116,7 @@ class OrganizationEditView extends React.PureComponent { }; render() { - if ( - this.state.isFetchingData || - !this.props.organization || - !this.state.pricingPlan || - !this.state.pricingPlanStatus - ) + if (this.state.isFetchingData || !this.props.organization || !this.state.pricingPlanStatus) return (
{ > Your Organization -

{this.state.displayName}

+

{this.props.organization.displayName}

{this.state.pricingPlanStatus.isExceeded ? ( @@ -207,8 +164,8 @@ class OrganizationEditView extends React.PureComponent { layout="vertical" ref={this.formRef} initialValues={{ - displayName: this.state.displayName, - newUserMailingList: this.state.newUserMailingList, + displayName: this.props.organization.displayName, + newUserMailingList: this.props.organization.newUserMailingList, }} > @@ -301,7 +258,7 @@ class OrganizationEditView extends React.PureComponent { } } -const mapStateToProps = (state: OxalisState): StateProps => ({ +const mapStateToProps = (state: OxalisState): Props => ({ organization: enforceActiveOrganization(state.activeOrganization), }); diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index a21367a476d..5f5dbeb0e84 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -42,18 +42,18 @@ export function isUserAllowedToRequestUpgrades(user: APIUser): boolean { return user.isAdmin || user.isOrganizationOwner; } -const PLAN_TO_RANK: Map = new Map([ - [PricingPlanEnum.Basic, 0], - [PricingPlanEnum.Team, 1], - [PricingPlanEnum.TeamTrial, 1], - [PricingPlanEnum.Power, 2], - [PricingPlanEnum.PowerTrial, 2], - [PricingPlanEnum.Custom, 2], -]); +const PLAN_TO_RANK = { + [PricingPlanEnum.Basic]: 0, + [PricingPlanEnum.Team]: 1, + [PricingPlanEnum.TeamTrial]: 1, + [PricingPlanEnum.Power]: 2, + [PricingPlanEnum.PowerTrial]: 2, + [PricingPlanEnum.Custom]: 2, +}; export function isPricingPlanGreaterEqualThan( planA: PricingPlanEnum, planB: PricingPlanEnum, ): boolean { - return PLAN_TO_RANK.get(planA) >= PLAN_TO_RANK.get(planB); + return PLAN_TO_RANK[planA] >= PLAN_TO_RANK[planB]; } diff --git a/frontend/javascripts/admin/user/user_list_view.tsx b/frontend/javascripts/admin/user/user_list_view.tsx index 25b0aec4937..ab721948506 100644 --- a/frontend/javascripts/admin/user/user_list_view.tsx +++ b/frontend/javascripts/admin/user/user_list_view.tsx @@ -65,7 +65,6 @@ type State = { activationFilter: Array<"true" | "false">; searchQuery: string; domainToEdit: string | null | undefined; - maxUserCountPerOrganization: number; }; const persistence = new Persistence>( { @@ -87,7 +86,6 @@ class UserListView extends React.PureComponent { searchQuery: "", singleSelectedUser: null, domainToEdit: null, - maxUserCountPerOrganization: this.props.activeOrganization.includedUsers, }; componentDidMount() { @@ -282,7 +280,7 @@ class UserListView extends React.PureComponent { message="You reached the maximum number of users" description={`You organization reached the maxmium number of users included in your current plan. Consider upgrading your WEBKNOSSOS plan to accommodate more users or deactivate some user accounts. Email invites are disabled in the meantime. Your organization currently has ${getActiveUserCount( this.state.users, - )} active users of ${this.state.maxUserCountPerOrganization} allowed by your plan.`} + )} active users of ${this.props.activeOrganization.includedUsers} allowed by your plan.`} type="error" showIcon style={{ @@ -345,7 +343,7 @@ class UserListView extends React.PureComponent { }; const noOtherUsers = this.state.users.length < 2; const isUserInvitesDisabled = - getActiveUserCount(this.state.users) >= this.state.maxUserCountPerOrganization; + getActiveUserCount(this.state.users) >= this.props.activeOrganization.includedUsers; return (
@@ -397,7 +395,7 @@ class UserListView extends React.PureComponent { { diff --git a/frontend/javascripts/components/pricing_enforcers.tsx b/frontend/javascripts/components/pricing_enforcers.tsx index dc97e71da87..e6a48640f76 100644 --- a/frontend/javascripts/components/pricing_enforcers.tsx +++ b/frontend/javascripts/components/pricing_enforcers.tsx @@ -3,18 +3,26 @@ import { useSelector } from "react-redux"; import { Tooltip, Menu, MenuItemProps, Alert, ButtonProps, Button } from "antd"; import { LockOutlined } from "@ant-design/icons"; import { PricingPlanEnum } from "admin/organization/organization_edit_view"; -import { isPricingPlanGreaterEqualThan } from "admin/organization/pricing_plan_utils"; +import { + isPricingPlanGreaterEqualThan, + isUserAllowedToRequestUpgrades, +} from "admin/organization/pricing_plan_utils"; import { Link } from "react-router-dom"; import type { MenuClickEventHandler } from "rc-menu/lib/interface"; import type { OxalisState } from "oxalis/store"; -const toolTipMessage = `This feature is not available in your organisation's plan. Ask your organisation owner to upgrade.`; +const toolTipMessage = `This feature is not available in your organization's plan. Ask your organization owner to upgrade.`; const handleMouseClick = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); }; +const handleMenuClick: MenuClickEventHandler = (info) => { + info.domEvent.preventDefault(); + info.domEvent.stopPropagation(); +}; + export function PricingEnforcedMenuItem({ children, requiredPricingPlan, @@ -32,11 +40,6 @@ export function PricingEnforcedMenuItem({ if (isFeatureAllowed) return {children}; - const handleMenuClick: MenuClickEventHandler = (info) => { - info.domEvent.preventDefault(); - info.domEvent.stopPropagation(); - }; - return ( state.activeOrganization?.name); + const activeUser = useSelector((state: OxalisState) => state.activeUser); + const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); + + const LinkToOrganizationSettings = + activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? ( + Go to Organization Settings + ) : null; return (
@@ -99,7 +108,7 @@ export function PageUnavailableForYourPlanView() { upgrading to a higher WEBKNOSSOS plan to unlock it or ask your organization's owner to upgrade.

- Go to Organization Settings + {LinkToOrganizationSettings} } type="error" diff --git a/frontend/javascripts/components/secured_route.tsx b/frontend/javascripts/components/secured_route.tsx index 443b6dd410a..ad7ad6b03ad 100644 --- a/frontend/javascripts/components/secured_route.tsx +++ b/frontend/javascripts/components/secured_route.tsx @@ -9,6 +9,7 @@ import { PageUnavailableForYourPlanView } from "components/pricing_enforcers"; import type { ComponentType } from "react"; import type { RouteComponentProps } from "react-router-dom"; import type { OxalisState } from "oxalis/store"; +import { enforceActiveOrganization } from "oxalis/model/accessors/organization_accessors"; type StateProps = { activeOrganization: APIOrganization | null; @@ -64,16 +65,14 @@ class SecuredRoute extends React.PureComponent { { - if (!isCompletelyAuthenticated) + if (!isCompletelyAuthenticated) { return ; + } + const organization = enforceActiveOrganization(this.props.activeOrganization); if ( this.props.requiredPricingPlan && - this.props.activeOrganization && - !isPricingPlanGreaterEqualThan( - this.props.activeOrganization.pricingPlan, - this.props.requiredPricingPlan, - ) + !isPricingPlanGreaterEqualThan(organization.pricingPlan, this.props.requiredPricingPlan) ) { return ; } diff --git a/frontend/javascripts/dashboard/dashboard_view.tsx b/frontend/javascripts/dashboard/dashboard_view.tsx index fffd5b3cf80..426464aee20 100644 --- a/frontend/javascripts/dashboard/dashboard_view.tsx +++ b/frontend/javascripts/dashboard/dashboard_view.tsx @@ -282,7 +282,6 @@ class DashboardView extends PureComponent { ) : null; this.state.pricingPlanStatus?.isAlmostExceeded; - // ToDo enable components below once pricing goes live const pricingPlanWarnings = this.props.activeOrganization && this.state.pricingPlanStatus?.isAlmostExceeded && diff --git a/frontend/javascripts/main.tsx b/frontend/javascripts/main.tsx index f048472c0d4..20fc3221ae5 100644 --- a/frontend/javascripts/main.tsx +++ b/frontend/javascripts/main.tsx @@ -73,16 +73,12 @@ async function loadHasOrganizations() { } async function loadOrganization() { - try { - const { activeUser } = Store.getState(); - if (activeUser) { - // organisation can only be loaded for user with a logged in wk account - // anonymous wk session for publicly shared datasets have no orga - const organization = await getOrganization(activeUser?.organization); - Store.dispatch(setActiveOrganizationAction(organization)); - } - } catch (e) { - // pass + const { activeUser } = Store.getState(); + if (activeUser) { + // organization can only be loaded for user with a logged in wk account + // anonymous wk session for publicly shared datasets have no orga + const organization = await getOrganization(activeUser.organization); + Store.dispatch(setActiveOrganizationAction(organization)); } } diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index f16ab999608..a402dded62a 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -375,7 +375,7 @@ instead. Only enable this option if you understand its effect. All layers will n "Your account has been created. An administrator is going to unlock you soon.", "auth.automatic_user_activation": "User was activated automatically", "auth.error_no_user": "No active user is logged in.", - "auth.error_no_orga": "No active organziation can be loaded.", + "auth.error_no_organization": "No active organziation can be loaded.", "auth.invalid_organization_name": "The link is not valid, since the specified organization does not exist. You are being redirected to the general registration form.", "request.max_item_count_alert": diff --git a/frontend/javascripts/oxalis/model/accessors/organization_accessors.ts b/frontend/javascripts/oxalis/model/accessors/organization_accessors.ts index b923f8ff67e..01b69ad0d41 100644 --- a/frontend/javascripts/oxalis/model/accessors/organization_accessors.ts +++ b/frontend/javascripts/oxalis/model/accessors/organization_accessors.ts @@ -7,6 +7,6 @@ export function enforceActiveOrganization( if (activeOrganization) { return activeOrganization; } else { - throw new Error(messages["auth.error_no_orga"]); + throw new Error(messages["auth.error_no_organization"]); } } diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.tsx index b70ac0d379c..0b6e5f12dda 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/dataset_info_tab_view.tsx @@ -251,7 +251,7 @@ class DatasetInfoTabView extends React.PureComponent { ) : null; } - getOrganisationLogo(isDatasetViewMode: boolean) { + getOrganizationLogo(isDatasetViewMode: boolean) { if (!this.props.dataset.logoUrl) { return null; } @@ -642,7 +642,7 @@ class DatasetInfoTabView extends React.PureComponent {
{this.getTracingStatistics()}
{this.getKeyboardShortcuts(isDatasetViewMode)} - {this.getOrganisationLogo(isDatasetViewMode)} + {this.getOrganizationLogo(isDatasetViewMode)}
); } From 0892679b1809efd95f08bcc4da49329c291ff6f4 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 20 Jan 2023 16:56:15 +0100 Subject: [PATCH 10/11] Update frontend/javascripts/components/pricing_enforcers.tsx Co-authored-by: Philipp Otto --- frontend/javascripts/components/pricing_enforcers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/components/pricing_enforcers.tsx b/frontend/javascripts/components/pricing_enforcers.tsx index e6a48640f76..d7c9b04800f 100644 --- a/frontend/javascripts/components/pricing_enforcers.tsx +++ b/frontend/javascripts/components/pricing_enforcers.tsx @@ -108,7 +108,7 @@ export function PageUnavailableForYourPlanView() { upgrading to a higher WEBKNOSSOS plan to unlock it or ask your organization's owner to upgrade.

- {LinkToOrganizationSettings} + {linkToOrganizationSettings} } type="error" From 5aaf1ba993fcdc9ece0d54235cd3bc85eed1f4fe Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 20 Jan 2023 16:56:28 +0100 Subject: [PATCH 11/11] Update frontend/javascripts/components/pricing_enforcers.tsx Co-authored-by: Philipp Otto --- frontend/javascripts/components/pricing_enforcers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/components/pricing_enforcers.tsx b/frontend/javascripts/components/pricing_enforcers.tsx index d7c9b04800f..0c44a1cf84b 100644 --- a/frontend/javascripts/components/pricing_enforcers.tsx +++ b/frontend/javascripts/components/pricing_enforcers.tsx @@ -88,7 +88,7 @@ export function PageUnavailableForYourPlanView() { const activeUser = useSelector((state: OxalisState) => state.activeUser); const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); - const LinkToOrganizationSettings = + const linkToOrganizationSettings = activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? ( Go to Organization Settings ) : null;