2023-05-01 18:24:23 +00:00
|
|
|
import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
|
2021-08-18 17:49:34 +00:00
|
|
|
import {getTheme} from 'app/client/ui/CustomThemes';
|
2020-10-02 15:10:00 +00:00
|
|
|
import {cssLeftPane} from 'app/client/ui/PagePanels';
|
2022-09-06 01:51:57 +00:00
|
|
|
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
2020-10-02 15:10:00 +00:00
|
|
|
import * as version from 'app/common/version';
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
import {menu, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
2023-07-26 22:31:02 +00:00
|
|
|
import {commonUrls} from 'app/common/gristUrls';
|
|
|
|
import {getOrgName, isTemplatesOrg, Organization} from 'app/common/UserAPI';
|
2021-08-18 17:49:34 +00:00
|
|
|
import {AppModel} from 'app/client/models/AppModel';
|
|
|
|
import {icon} from 'app/client/ui2018/icons';
|
2021-11-05 14:47:17 +00:00
|
|
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
|
|
|
import * as roles from 'app/common/roles';
|
2022-06-03 14:58:07 +00:00
|
|
|
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
2023-07-26 22:31:02 +00:00
|
|
|
import {Computed, Disposable, dom, DomContents, styled} from 'grainjs';
|
2022-10-28 16:11:08 +00:00
|
|
|
import {makeT} from 'app/client/lib/localization';
|
2023-06-06 17:08:50 +00:00
|
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
2022-10-28 16:11:08 +00:00
|
|
|
|
|
|
|
const t = makeT('AppHeader');
|
2021-08-18 17:49:34 +00:00
|
|
|
|
2022-06-03 14:58:07 +00:00
|
|
|
// Maps a name of a Product (from app/gen-server/entity/Product.ts) to a tag (pill) to show next
|
|
|
|
// to the org name.
|
|
|
|
const productPills: {[name: string]: string|null} = {
|
|
|
|
// TODO We don't label paid team plans with a tag yet, but we should label as "Pro" once we
|
|
|
|
// update our pricing pages to refer to paid team plans as Pro plans.
|
|
|
|
"professional": null, // Deprecated but used in development.
|
|
|
|
"team": null, // Used for the paid team plans.
|
|
|
|
"teamFree": "Free", // The new free team plan.
|
|
|
|
// Other plans are either personal, or grandfathered, or for testing.
|
|
|
|
};
|
2021-08-18 17:49:34 +00:00
|
|
|
|
2023-07-26 22:31:02 +00:00
|
|
|
interface AppLogoOrgNameAndLink {
|
|
|
|
name: string;
|
|
|
|
link: AppLogoLink;
|
|
|
|
org?: string;
|
|
|
|
href?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
type AppLogoLink = AppLogoOrgDomain | AppLogoHref;
|
|
|
|
|
|
|
|
interface AppLogoOrgDomain {
|
|
|
|
type: 'domain';
|
|
|
|
domain: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface AppLogoHref {
|
|
|
|
type: 'href';
|
|
|
|
href: string;
|
|
|
|
}
|
|
|
|
|
2021-08-18 17:49:34 +00:00
|
|
|
export class AppHeader extends Disposable {
|
2023-07-26 22:31:02 +00:00
|
|
|
private _currentOrg = this._appModel.currentOrg;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The name and link of the site shown next to the logo.
|
|
|
|
*
|
|
|
|
* The last visited site is used, if known. Otherwise, the current site is used.
|
|
|
|
*/
|
|
|
|
private _appLogoOrg = Computed.create<AppLogoOrgNameAndLink>(this, (use) => {
|
|
|
|
const availableOrgs = use(this._appModel.topAppModel.orgs);
|
|
|
|
const currentOrgName = (this._appModel.currentOrgName ||
|
|
|
|
(this._docPageModel && use(this._docPageModel.currentOrgName))) ?? '';
|
|
|
|
const lastVisitedOrgDomain = use(this._appModel.lastVisitedOrgDomain);
|
|
|
|
return this._getAppLogoOrgNameAndLink({availableOrgs, currentOrgName, lastVisitedOrgDomain});
|
|
|
|
});
|
|
|
|
|
|
|
|
private _appLogoOrgName = Computed.create(this, this._appLogoOrg, (_use, {name}) => name);
|
|
|
|
|
|
|
|
private _appLogoOrgLink = Computed.create(this, this._appLogoOrg, (_use, {link}) => link);
|
|
|
|
|
|
|
|
constructor(private _appModel: AppModel, private _docPageModel?: DocPageModel) {
|
2021-08-18 17:49:34 +00:00
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
public buildDom() {
|
2022-09-06 01:51:57 +00:00
|
|
|
const productFlavor = getTheme(this._appModel.topAppModel.productFlavor);
|
2021-08-18 17:49:34 +00:00
|
|
|
|
|
|
|
return cssAppHeader(
|
2022-09-06 01:51:57 +00:00
|
|
|
cssAppHeader.cls('-widelogo', productFlavor.wideLogo || false),
|
2023-07-26 22:31:02 +00:00
|
|
|
dom.domComputed(this._appLogoOrgLink, orgLink => cssAppLogo(
|
|
|
|
// Show version when hovering over the application icon.
|
|
|
|
// Include gitcommit when known. Cast version.gitcommit since, depending
|
|
|
|
// on how Grist is compiled, tsc may believe it to be a constant and
|
|
|
|
// believe that testing it is unnecessary.
|
2022-10-12 14:49:14 +00:00
|
|
|
{title: `Version ${version.version}` +
|
|
|
|
((version.gitcommit as string) !== 'unknown' ? ` (${version.gitcommit})` : '')},
|
2023-07-26 22:31:02 +00:00
|
|
|
this._setHomePageUrl(orgLink),
|
2021-08-18 17:49:34 +00:00
|
|
|
testId('dm-logo')
|
2023-07-26 22:31:02 +00:00
|
|
|
)),
|
|
|
|
this._buildOrgLinkOrMenu(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private _buildOrgLinkOrMenu() {
|
|
|
|
const {currentValidUser, isPersonal, isTemplatesSite} = this._appModel;
|
|
|
|
if (!currentValidUser && (isPersonal || isTemplatesSite)) {
|
|
|
|
return cssOrgLink(
|
|
|
|
cssOrgName(dom.text(this._appLogoOrgName), testId('dm-orgname')),
|
|
|
|
{href: commonUrls.templates},
|
|
|
|
testId('dm-org'),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return cssOrg(
|
|
|
|
cssOrgName(dom.text(this._appLogoOrgName), testId('dm-orgname')),
|
|
|
|
productPill(this._currentOrg),
|
|
|
|
dom.maybe(this._appLogoOrgName, () => cssDropdownIcon('Dropdown')),
|
2021-11-05 14:47:17 +00:00
|
|
|
menu(() => [
|
2022-07-26 17:49:35 +00:00
|
|
|
menuSubHeader(
|
2023-07-26 22:31:02 +00:00
|
|
|
this._appModel.isPersonal
|
|
|
|
? t("Personal Site") + (this._appModel.isLegacySite ? ` (${t("Legacy")})` : '')
|
|
|
|
: t("Team Site"),
|
2022-07-26 17:49:35 +00:00
|
|
|
testId('orgmenu-title'),
|
|
|
|
),
|
2022-12-06 13:57:29 +00:00
|
|
|
menuItemLink(urlState().setLinkUrl({}), t("Home Page"), testId('orgmenu-home-page')),
|
2021-08-18 17:49:34 +00:00
|
|
|
|
2021-11-05 14:47:17 +00:00
|
|
|
// Show 'Organization Settings' when on a home page of a valid org.
|
2023-07-26 22:31:02 +00:00
|
|
|
(!this._docPageModel && this._currentOrg && !this._currentOrg.owner ?
|
2022-06-03 14:58:07 +00:00
|
|
|
menuItem(() => manageTeamUsersApp(this._appModel),
|
|
|
|
'Manage Team', testId('orgmenu-manage-team'),
|
2023-07-26 22:31:02 +00:00
|
|
|
dom.cls('disabled', !roles.canEditAccess(this._currentOrg.access))) :
|
2021-11-05 14:47:17 +00:00
|
|
|
// Don't show on doc pages, or for personal orgs.
|
|
|
|
null),
|
|
|
|
|
2023-06-06 17:08:50 +00:00
|
|
|
this._maybeBuildBillingPageMenuItem(),
|
|
|
|
this._maybeBuildActivationPageMenuItem(),
|
2021-08-18 17:49:34 +00:00
|
|
|
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
maybeAddSiteSwitcherSection(this._appModel),
|
2021-11-05 14:47:17 +00:00
|
|
|
], { placement: 'bottom-start' }),
|
|
|
|
testId('dm-org'),
|
2023-07-26 22:31:02 +00:00
|
|
|
);
|
|
|
|
}
|
2021-08-18 17:49:34 +00:00
|
|
|
}
|
2023-05-01 18:24:23 +00:00
|
|
|
|
2023-07-26 22:31:02 +00:00
|
|
|
private _setHomePageUrl(link: AppLogoLink) {
|
|
|
|
if (link.type === 'href') {
|
|
|
|
return {href: link.href};
|
2023-05-01 18:24:23 +00:00
|
|
|
} else {
|
2023-07-26 22:31:02 +00:00
|
|
|
return urlState().setLinkUrl({org: link.domain});
|
2023-05-01 18:24:23 +00:00
|
|
|
}
|
|
|
|
}
|
2023-06-06 17:08:50 +00:00
|
|
|
|
|
|
|
private _maybeBuildBillingPageMenuItem() {
|
|
|
|
const {deploymentType} = getGristConfig();
|
|
|
|
if (deploymentType !== 'saas') { return null; }
|
|
|
|
|
|
|
|
const {currentOrg} = this._appModel;
|
|
|
|
const isBillingManager = this._appModel.isBillingManager() || this._appModel.isSupport();
|
|
|
|
return currentOrg && !currentOrg.owner ?
|
|
|
|
// For links, disabling with just a class is hard; easier to just not make it a link.
|
|
|
|
// TODO weasel menus should support disabling menuItemLink.
|
|
|
|
(isBillingManager
|
|
|
|
? menuItemLink(
|
|
|
|
urlState().setLinkUrl({billing: 'billing'}),
|
|
|
|
'Billing Account',
|
|
|
|
testId('orgmenu-billing'),
|
|
|
|
)
|
|
|
|
: menuItem(
|
|
|
|
() => null,
|
|
|
|
'Billing Account',
|
|
|
|
dom.cls('disabled', true),
|
|
|
|
testId('orgmenu-billing'),
|
|
|
|
)
|
|
|
|
) :
|
|
|
|
null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private _maybeBuildActivationPageMenuItem() {
|
|
|
|
const {activation, deploymentType} = getGristConfig();
|
|
|
|
if (deploymentType !== 'enterprise' || !activation?.isManager) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return menuItemLink('Activation', urlState().setLinkUrl({activation: 'activation'}));
|
|
|
|
}
|
2023-07-26 22:31:02 +00:00
|
|
|
|
|
|
|
private _getAppLogoOrgNameAndLink(params: {
|
|
|
|
availableOrgs: Organization[],
|
|
|
|
currentOrgName: string,
|
|
|
|
lastVisitedOrgDomain: string|null,
|
|
|
|
}): AppLogoOrgNameAndLink {
|
|
|
|
const {
|
|
|
|
currentValidUser,
|
|
|
|
isPersonal,
|
|
|
|
isTemplatesSite,
|
|
|
|
} = this._appModel;
|
|
|
|
if (!currentValidUser && (isPersonal || isTemplatesSite)) {
|
|
|
|
// When signed out and not on a team site, link to the templates site.
|
|
|
|
return {
|
|
|
|
name: t('Grist Templates'),
|
|
|
|
link: {
|
|
|
|
type: 'href',
|
|
|
|
href: commonUrls.templates,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const {availableOrgs, currentOrgName, lastVisitedOrgDomain} = params;
|
|
|
|
if (lastVisitedOrgDomain) {
|
|
|
|
const lastVisitedOrg = availableOrgs.find(({domain}) => domain === lastVisitedOrgDomain);
|
|
|
|
if (lastVisitedOrg) {
|
|
|
|
return {
|
|
|
|
name: getOrgName(lastVisitedOrg),
|
|
|
|
link: {
|
|
|
|
type: 'domain',
|
|
|
|
domain: lastVisitedOrgDomain,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: currentOrgName ?? '',
|
|
|
|
link: {
|
|
|
|
type: 'href',
|
|
|
|
href: getWelcomeHomeUrl(),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
2022-06-03 14:58:07 +00:00
|
|
|
export function productPill(org: Organization|null, options: {large?: boolean} = {}): DomContents {
|
|
|
|
if (!org || isTemplatesOrg(org)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const product = org?.billingAccount?.product.name;
|
|
|
|
const pillTag = product && productPills[product];
|
|
|
|
if (!pillTag) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return cssProductPill(cssProductPill.cls('-' + pillTag),
|
|
|
|
options.large ? cssProductPill.cls('-large') : null,
|
|
|
|
pillTag,
|
|
|
|
testId('appheader-product-pill'));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-08-18 17:49:34 +00:00
|
|
|
const cssAppHeader = styled('div', `
|
2020-10-02 15:10:00 +00:00
|
|
|
display: flex;
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
align-items: center;
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.leftPanelBg};
|
2020-10-02 15:10:00 +00:00
|
|
|
&, &:hover, &:focus {
|
|
|
|
text-decoration: none;
|
|
|
|
outline: none;
|
2022-09-06 01:51:57 +00:00
|
|
|
color: ${theme.text};
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2021-08-18 17:49:34 +00:00
|
|
|
const cssAppLogo = styled('a', `
|
2020-10-02 15:10:00 +00:00
|
|
|
flex: none;
|
|
|
|
height: 48px;
|
|
|
|
width: 48px;
|
|
|
|
background-image: var(--icon-GristLogo);
|
2022-05-12 06:08:06 +00:00
|
|
|
background-size: ${vars.logoSize};
|
2020-10-02 15:10:00 +00:00
|
|
|
background-repeat: no-repeat;
|
|
|
|
background-position: center;
|
|
|
|
background-color: ${vars.logoBg};
|
|
|
|
.${cssAppHeader.className}-widelogo & {
|
|
|
|
width: 100%;
|
|
|
|
background-size: contain;
|
|
|
|
background-origin: content-box;
|
|
|
|
padding: 8px;
|
|
|
|
}
|
|
|
|
.${cssLeftPane.className}-open .${cssAppHeader.className}-widelogo & {
|
|
|
|
background-image: var(--icon-GristWideLogo, var(--icon-GristLogo));
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2021-08-18 17:49:34 +00:00
|
|
|
const cssDropdownIcon = styled(icon, `
|
2022-09-06 01:51:57 +00:00
|
|
|
--icon-color: ${theme.text};
|
2021-08-18 17:49:34 +00:00
|
|
|
flex-shrink: 0;
|
|
|
|
margin-right: 8px;
|
|
|
|
`);
|
|
|
|
|
|
|
|
const cssOrg = styled('div', `
|
2022-07-27 19:02:28 +00:00
|
|
|
display: none;
|
2021-08-18 17:49:34 +00:00
|
|
|
flex-grow: 1;
|
|
|
|
align-items: center;
|
|
|
|
max-width: calc(100% - 48px);
|
|
|
|
cursor: pointer;
|
|
|
|
height: 100%;
|
2022-06-03 14:58:07 +00:00
|
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
&:hover {
|
2022-09-06 01:51:57 +00:00
|
|
|
background-color: ${theme.hover};
|
2022-06-03 14:58:07 +00:00
|
|
|
}
|
2022-07-27 19:02:28 +00:00
|
|
|
|
|
|
|
.${cssLeftPane.className}-open & {
|
|
|
|
display: flex;
|
|
|
|
}
|
2021-08-18 17:49:34 +00:00
|
|
|
`);
|
|
|
|
|
2023-07-26 22:31:02 +00:00
|
|
|
const cssOrgLink = styled('a', `
|
|
|
|
display: none;
|
|
|
|
flex-grow: 1;
|
|
|
|
align-items: center;
|
|
|
|
max-width: calc(100% - 48px);
|
|
|
|
cursor: pointer;
|
|
|
|
height: 100%;
|
|
|
|
font-weight: 500;
|
|
|
|
color: ${theme.text};
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
&, &:hover, &:focus {
|
|
|
|
text-decoration: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
color: ${theme.text};
|
|
|
|
background-color: ${theme.hover};
|
|
|
|
}
|
|
|
|
|
|
|
|
.${cssLeftPane.className}-open & {
|
|
|
|
display: flex;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
const cssOrgName = styled('div', `
|
2021-08-18 17:49:34 +00:00
|
|
|
padding-left: 16px;
|
|
|
|
padding-right: 8px;
|
2020-10-02 15:10:00 +00:00
|
|
|
white-space: nowrap;
|
|
|
|
overflow: hidden;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
.${cssAppHeader.className}-widelogo & {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
`);
|
2022-06-03 14:58:07 +00:00
|
|
|
|
|
|
|
const cssProductPill = styled('div', `
|
|
|
|
border-radius: 4px;
|
|
|
|
font-size: ${vars.smallFontSize};
|
|
|
|
padding: 2px 4px;
|
|
|
|
display: inline;
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
|
|
&-Free {
|
|
|
|
background-color: ${colors.orange};
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
&-Pro {
|
|
|
|
background-color: ${colors.lightGreen};
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
&-large {
|
|
|
|
padding: 4px 8px;
|
|
|
|
margin-left: 16px;
|
|
|
|
font-size: ${vars.mediumFontSize};
|
|
|
|
}
|
|
|
|
`);
|