(core) Admin Panel and InstallAdmin class to identify installation admins.

Summary:
- Add InstallAdmin class to identify users who can manage Grist installation.

  This is overridable by different Grist flavors (e.g. different in SaaS).
  It generalizes previous logic used to decide who can control Activation
  settings (e.g. enable telemetry).

- Implement a basic Admin Panel at /admin, and move items previously in the
  "Support Grist" page into the "Support Grist" section of the Admin Panel.

- Replace "Support Grist" menu items with "Admin Panel" and show only to admins.

- Add "Support Grist" links to Github sponsorship to user-account menu.

- Add "Support Grist" button to top-bar, which
  - for admins, replaces the previous "Contribute" button and reopens the "Support Grist / opt-in to telemetry" nudge (unchanged)
  - for everyone else, links to Github sponsorship
  - in either case, user can dismiss it.

Test Plan: Shuffled some test cases between Support Grist and the new Admin Panel, and added some new cases.

Reviewers: jarek, paulfitz

Reviewed By: jarek, paulfitz

Differential Revision: https://phab.getgrist.com/D4194
This commit is contained in:
Dmitry S
2024-03-23 13:11:06 -04:00
parent 0c05f4cdc4
commit e380fcfa90
32 changed files with 875 additions and 524 deletions

195
test/nbrowser/AdminPanel.ts Normal file
View File

@@ -0,0 +1,195 @@
import {TelemetryLevel} from 'app/common/Telemetry';
import {assert, driver, Key, WebElement} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import * as testUtils from 'test/server/testUtils';
describe('AdminPanel', function() {
this.timeout(30000);
setupTestSuite();
let oldEnv: testUtils.EnvironmentSnapshot;
let session: gu.Session;
afterEach(() => gu.checkForErrors());
before(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
process.env.GRIST_TEST_SERVER_DEPLOYMENT_TYPE = 'core';
process.env.GRIST_DEFAULT_EMAIL = gu.session().email;
await server.restart(true);
});
after(async function() {
oldEnv.restore();
await server.restart(true);
});
it('should not be shown to non-managers', async function() {
session = await gu.session().user('user2').personalSite.login();
await session.loadDocMenu('/');
await gu.openAccountMenu();
assert.equal(await driver.find('.test-usermenu-admin-panel').isPresent(), false);
await driver.sendKeys(Key.ESCAPE);
assert.equal(await driver.find('.test-dm-admin-panel').isPresent(), false);
// Try loading the URL directly.
await driver.get(`${server.getHost()}/admin`);
assert.match(await driver.findWait('.test-error-header', 2000).getText(), /Access denied/);
assert.equal(await driver.find('.test-admin-panel').isPresent(), false);
});
it('should be shown to managers', async function() {
session = await gu.session().personalSite.login();
await session.loadDocMenu('/');
assert.equal(await driver.find('.test-dm-admin-panel').isDisplayed(), true);
assert.match(await driver.find('.test-dm-admin-panel').getAttribute('href'), /\/admin$/);
await gu.openAccountMenu();
assert.equal(await driver.find('.test-usermenu-admin-panel').isDisplayed(), true);
assert.match(await driver.find('.test-usermenu-admin-panel').getAttribute('href'), /\/admin$/);
await driver.find('.test-usermenu-admin-panel').click();
assert.equal(await waitForAdminPanel().isDisplayed(), true);
});
it('should include support-grist section', async function() {
assert.match(await driver.find('.test-admin-panel-item-sponsor').getText(), /Support Grist Labs on GitHub/);
await withExpandedItem('sponsor', async () => {
const button = await driver.find('.test-support-grist-page-sponsorship-section');
assert.equal(await button.isDisplayed(), true);
assert.match(await button.getText(), /You can support Grist open-source/);
});
});
it('supports opting in to telemetry from the page', async function() {
await assertTelemetryLevel('off');
let toggle = driver.find('.test-admin-panel-item-value-telemetry .widget_switch');
assert.equal(await isSwitchOn(toggle), false);
await withExpandedItem('telemetry', async () => {
assert.isFalse(await driver.find('.test-support-grist-page-telemetry-section-message').isPresent());
await driver.findContentWait(
'.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000).click();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000);
assert.equal(
await driver.find('.test-support-grist-page-telemetry-section-message').getText(),
'You have opted in to telemetry. Thank you! 🙏'
);
assert.equal(await isSwitchOn(toggle), true);
});
// Check it's still on after collapsing.
assert.equal(await isSwitchOn(toggle), true);
// Reload the page and check that the Grist config indicates telemetry is set to "limited".
await driver.navigate().refresh();
await waitForAdminPanel();
toggle = driver.find('.test-admin-panel-item-value-telemetry .widget_switch');
assert.equal(await isSwitchOn(toggle), true);
await toggleItem('telemetry');
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000);
assert.equal(
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
'You have opted in to telemetry. Thank you! 🙏'
);
await assertTelemetryLevel('limited');
});
it('supports opting out of telemetry from the page', async function() {
await driver.findContent('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/).click();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
assert.isFalse(await driver.find('.test-support-grist-page-telemetry-section-message').isPresent());
let toggle = driver.find('.test-admin-panel-item-value-telemetry .widget_switch');
assert.equal(await isSwitchOn(toggle), false);
// Reload the page and check that the Grist config indicates telemetry is set to "off".
await driver.navigate().refresh();
await waitForAdminPanel();
await toggleItem('telemetry');
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
assert.isFalse(await driver.find('.test-support-grist-page-telemetry-section-message').isPresent());
await assertTelemetryLevel('off');
toggle = driver.find('.test-admin-panel-item-value-telemetry .widget_switch');
assert.equal(await isSwitchOn(toggle), false);
});
it('supports toggling telemetry from the toggle in the top line', async function() {
const toggle = driver.find('.test-admin-panel-item-value-telemetry .widget_switch');
assert.equal(await isSwitchOn(toggle), false);
await toggle.click();
await gu.waitForServer();
assert.equal(await isSwitchOn(toggle), true);
assert.match(await driver.find('.test-support-grist-page-telemetry-section-message').getText(),
/You have opted in/);
await toggle.click();
await gu.waitForServer();
assert.equal(await isSwitchOn(toggle), false);
await withExpandedItem('telemetry', async () => {
assert.equal(await driver.find('.test-support-grist-page-telemetry-section-message').isPresent(), false);
});
});
it('shows telemetry opt-in status even when set via environment variable', async function() {
// Set the telemetry level to "limited" via environment variable and restart the server.
process.env.GRIST_TELEMETRY_LEVEL = 'limited';
await server.restart();
// Check that the Support Grist page reports telemetry is enabled.
await driver.get(`${server.getHost()}/admin`);
await waitForAdminPanel();
const toggle = driver.find('.test-admin-panel-item-value-telemetry .widget_switch');
assert.equal(await isSwitchOn(toggle), true);
await toggleItem('telemetry');
assert.equal(
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
'You have opted in to telemetry. Thank you! 🙏'
);
assert.isFalse(await driver.findContent('.test-support-grist-page-telemetry-section button',
/Opt out of Telemetry/).isPresent());
// Now set the telemetry level to "off" and restart the server.
process.env.GRIST_TELEMETRY_LEVEL = 'off';
await server.restart();
// Check that the Support Grist page reports telemetry is disabled.
await driver.get(`${server.getHost()}/admin`);
await waitForAdminPanel();
await toggleItem('telemetry');
assert.equal(
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
'You have opted out of telemetry.'
);
assert.isFalse(await driver.findContent('.test-support-grist-page-telemetry-section button',
/Opt in to Telemetry/).isPresent());
});
it('should show version', async function() {
await driver.get(`${server.getHost()}/admin`);
await waitForAdminPanel();
assert.equal(await driver.find('.test-admin-panel-item-version').isDisplayed(), true);
assert.match(await driver.find('.test-admin-panel-item-value-version').getText(), /^Version \d+\./);
});
});
async function assertTelemetryLevel(level: TelemetryLevel) {
const telemetryLevel = await driver.executeScript('return window.gristConfig.telemetry?.telemetryLevel');
assert.equal(telemetryLevel, level);
}
async function toggleItem(itemId: string) {
const header = await driver.find(`.test-admin-panel-item-name-${itemId}`);
await header.click();
await driver.sleep(500); // Time to expand or collapse.
return header;
}
async function withExpandedItem(itemId: string, callback: () => Promise<void>) {
const header = await toggleItem(itemId);
await callback();
await header.click();
await driver.sleep(500); // Time to collapse.
}
const isSwitchOn = (switchElem: WebElement) => switchElem.matches('[class*=switch_on]');
const waitForAdminPanel = () => driver.findWait('.test-admin-panel', 2000);

View File

@@ -5,6 +5,8 @@ import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import * as testUtils from 'test/server/testUtils';
const sponsorshipUrl = 'https://github.com/sponsors/gristlabs';
describe('SupportGrist', function() {
this.timeout(30000);
setupTestSuite();
@@ -14,6 +16,10 @@ describe('SupportGrist', function() {
afterEach(() => gu.checkForErrors());
after(async function() {
await server.restart();
});
describe('in grist-core', function() {
before(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
@@ -24,133 +30,66 @@ describe('SupportGrist', function() {
after(async function() {
oldEnv.restore();
await server.restart();
});
describe('when user is not a manager', function() {
before(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
await server.restart();
session = await gu.session().user('user2').personalSite.login();
await session.loadDocMenu('/');
});
after(async function() {
oldEnv.restore();
});
it('does not show a nudge on the doc menu', async function() {
await assertNudgeButtonShown(false);
await assertNudgeCardShown(false);
await assertSupportButtonShown(true, {isSponsorLink: true});
});
it('shows a link to the Support Grist page in the user menu', async function() {
await gu.openAccountMenu();
await driver.find('.test-usermenu-support-grist').click();
assert.isTrue(await driver.findContentWait(
'.test-support-grist-page-sponsorship-section',
/Sponsor Grist Labs on GitHub/,
4000
).isDisplayed());
});
it('shows a message that telemetry is managed by the site administrator', async function() {
assert.isTrue(await driver.findContentWait(
'.test-support-grist-page-telemetry-section',
/This instance is opted out of telemetry\. Only the site administrator has permission to change this\./,
4000
).isDisplayed());
process.env.GRIST_TELEMETRY_LEVEL = 'limited';
await server.restart();
await driver.navigate().refresh();
assert.isTrue(await driver.findContentWait(
'.test-support-grist-page-telemetry-section',
/This instance is opted in to telemetry\. Only the site administrator has permission to change this\./,
4000
).isDisplayed());
await assertMenuHasAdminPanel(false);
await assertMenuHasSupportGrist(true);
});
});
describe('when user is a manager', function() {
before(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
await server.restart();
session = await gu.session().personalSite.login();
await session.loadDocMenu('/');
});
after(async function() {
oldEnv.restore();
});
it('shows a nudge on the doc menu', async function() {
// Check that the nudge is expanded by default.
await assertNudgeButtonShown(false);
await assertSupportButtonShown(false);
await assertNudgeCardShown(true);
// Reload the doc menu and check that it's still expanded.
await session.loadDocMenu('/');
await assertNudgeButtonShown(false);
await assertSupportButtonShown(false);
await assertNudgeCardShown(true);
// Close the nudge and check that it's now collapsed.
await driver.find('.test-support-grist-nudge-card-close').click();
await assertNudgeButtonShown(true);
await driver.find('.test-support-nudge-close').click();
await assertSupportButtonShown(true, {isSponsorLink: false});
await assertNudgeCardShown(false);
// Reload again, and check that it's still collapsed.
await session.loadDocMenu('/');
await assertNudgeButtonShown(true);
await assertSupportButtonShown(true, {isSponsorLink: false});
await assertNudgeCardShown(false);
// Dismiss the contribute button and check that it's now gone, even after reloading.
await driver.find('.test-support-grist-nudge-contribute-button').mouseMove();
await driver.find('.test-support-grist-nudge-contribute-button-close').click();
await assertNudgeButtonShown(false);
await driver.find('.test-support-grist-button').mouseMove();
await driver.find('.test-support-grist-button-dismiss').click();
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
await session.loadDocMenu('/');
await assertNudgeButtonShown(false);
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
});
it('shows a link to the Support Grist page in the user menu', async function() {
it('shows a link to Admin Panel and Support Grist in the user menu', async function() {
await gu.openAccountMenu();
await driver.find('.test-usermenu-support-grist').click();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
assert.isFalse(await driver.find('.test-support-grist-page-telemetry-section-message').isPresent());
});
it('supports opting in to telemetry from the page', async function() {
await assertTelemetryLevel('off');
await driver.findContentWait(
'.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000).click();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000);
assert.equal(
await driver.find('.test-support-grist-page-telemetry-section-message').getText(),
'You have opted in to telemetry. Thank you! 🙏'
);
// Reload the page and check that the Grist config indicates telemetry is set to "limited".
await driver.navigate().refresh();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000);
assert.equal(
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
'You have opted in to telemetry. Thank you! 🙏'
);
await assertTelemetryLevel('limited');
});
it('supports opting out of telemetry from the page', async function() {
await driver.findContent('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/).click();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
assert.isFalse(await driver.find('.test-support-grist-page-telemetry-section-message').isPresent());
// Reload the page and check that the Grist config indicates telemetry is set to "off".
await driver.navigate().refresh();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
assert.isFalse(await driver.find('.test-support-grist-page-telemetry-section-message').isPresent());
await assertTelemetryLevel('off');
await assertMenuHasAdminPanel(true);
await assertMenuHasSupportGrist(true);
});
it('supports opting in to telemetry from the nudge', async function() {
@@ -160,14 +99,14 @@ describe('SupportGrist', function() {
await session.loadDocMenu('/');
// Opt in to telemetry and reload the page.
await driver.find('.test-support-grist-nudge-card-opt-in').click();
await driver.findWait('.test-support-grist-nudge-card-close-button', 1000).click();
await assertNudgeButtonShown(false);
await driver.find('.test-support-nudge-opt-in').click();
await driver.findWait('.test-support-nudge-close-button', 1000).click();
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
await session.loadDocMenu('/');
// Check that the nudge is no longer shown and telemetry is set to "limited".
await assertNudgeButtonShown(false);
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
await assertTelemetryLevel('limited');
});
@@ -179,52 +118,55 @@ describe('SupportGrist', function() {
// Reload the doc menu and check that the nudge still isn't shown.
await session.loadDocMenu('/');
await assertNudgeButtonShown(false);
await assertNudgeCardShown(false);
// We still show the "Support Grist" button linking to sponsorship page.
await assertSupportButtonShown(true, {isSponsorLink: true});
// Disable telemetry from the Support Grist page.
await gu.openAccountMenu();
await driver.find('.test-usermenu-support-grist').click();
await driver.find('.test-usermenu-admin-panel').click();
await driver.findWait('.test-admin-panel', 2000);
await driver.find('.test-admin-panel-item-name-telemetry').click();
await driver.sleep(500); // Wait for section to expand.
await driver.findContentWait(
'.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000).click();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
// Reload the doc menu and check that the nudge is now shown.
await gu.loadDocMenu('/');
await assertNudgeButtonShown(false);
await assertSupportButtonShown(false);
await assertNudgeCardShown(true);
});
it('shows telemetry opt-in status even when set via environment variable', async function() {
// Set the telemetry level to "limited" via environment variable and restart the server.
process.env.GRIST_TELEMETRY_LEVEL = 'limited';
await server.restart();
it('shows sponsorship link when no telemetry nudge, and allows dismissing it', async function() {
// Reset all dismissed popups, including the telemetry nudge.
await driver.executeScript('resetDismissedPopups();');
await gu.waitForServer();
// Check that the Support Grist page reports telemetry is enabled.
await gu.loadDocMenu('/');
await gu.openAccountMenu();
await driver.find('.test-usermenu-support-grist').click();
assert.equal(
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
'You have opted in to telemetry. Thank you! 🙏'
);
assert.isFalse(await driver.findContent('.test-support-grist-page-telemetry-section button',
/Opt out of Telemetry/).isPresent());
// Opt in to telemetry
const api = session.createHomeApi();
await api.testRequest(`${api.getBaseUrl()}/api/install/prefs`, {
method: 'patch',
body: JSON.stringify({telemetry: {telemetryLevel: 'limited'}}),
});
// Now set the telemetry level to "off" and restart the server.
process.env.GRIST_TELEMETRY_LEVEL = 'off';
await server.restart();
await session.loadDocMenu('/');
await assertTelemetryLevel('limited');
// Check that the Support Grist page reports telemetry is disabled.
await gu.loadDocMenu('/');
await gu.openAccountMenu();
await driver.find('.test-usermenu-support-grist').click();
assert.equal(
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
'You have opted out of telemetry.'
);
assert.isFalse(await driver.findContent('.test-support-grist-page-telemetry-section button',
/Opt in to Telemetry/).isPresent());
// We still show the "Support Grist" button linking to sponsorship page.
await assertSupportButtonShown(true, {isSponsorLink: true});
await assertNudgeCardShown(false);
// we can dismiss it.
await driver.find('.test-support-grist-button').mouseMove();
await driver.find('.test-support-grist-button-dismiss').click();
await assertSupportButtonShown(false);
// And this will get remembered.
await session.loadDocMenu('/');
await assertNudgeCardShown(false);
await assertSupportButtonShown(false);
});
});
});
@@ -241,17 +183,25 @@ describe('SupportGrist', function() {
after(async function() {
oldEnv.restore();
await server.restart();
});
it('does not show a nudge on the doc menu', async function() {
await assertNudgeButtonShown(false);
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
});
it('does not show a link to the Support Grist page in the user menu', async function() {
it('shows Admin Panel but not Support Grist in the user menu for admin', async function() {
await gu.openAccountMenu();
assert.isFalse(await driver.find('.test-usermenu-support-grist').isPresent());
await assertMenuHasAdminPanel(true);
await assertMenuHasSupportGrist(false);
});
it('does not show Admin Panel or Support Grist in the user menu for non-admin', async function() {
session = await gu.session().user('user2').personalSite.login();
await session.loadDocMenu('/');
await gu.openAccountMenu();
await assertMenuHasAdminPanel(false);
await assertMenuHasSupportGrist(false);
});
});
@@ -267,38 +217,49 @@ describe('SupportGrist', function() {
after(async function() {
oldEnv.restore();
await server.restart();
});
it('does not show a nudge on the doc menu', async function() {
await assertNudgeButtonShown(false);
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
});
it('does not show a link to the Support Grist page in the user menu', async function() {
it('shows Admin Panel but not Support Grist page in the user menu', async function() {
await gu.openAccountMenu();
assert.isFalse(await driver.find('.test-usermenu-support-grist').isPresent());
await assertMenuHasAdminPanel(true);
await assertMenuHasSupportGrist(false);
});
});
});
async function assertNudgeButtonShown(isShown: boolean) {
async function assertSupportButtonShown(isShown: false): Promise<void>;
async function assertSupportButtonShown(isShown: true, opts: {isSponsorLink: boolean}): Promise<void>;
async function assertSupportButtonShown(isShown: boolean, opts?: {isSponsorLink: boolean}) {
const button = driver.find('.test-support-grist-button');
assert.equal(await button.isPresent() && await button.isDisplayed(), isShown);
if (isShown) {
assert.isTrue(
await driver.find('.test-support-grist-nudge-contribute-button').isDisplayed()
);
} else {
assert.isFalse(await driver.find('.test-support-grist-nudge-contribute-button').isPresent());
assert.equal(await button.getAttribute('href'), opts?.isSponsorLink ? sponsorshipUrl : null);
}
}
async function assertNudgeCardShown(isShown: boolean) {
const card = driver.find('.test-support-nudge');
assert.equal(await card.isPresent() && await card.isDisplayed(), isShown);
}
async function assertMenuHasAdminPanel(isShown: boolean) {
const elem = driver.find('.test-usermenu-admin-panel');
assert.equal(await elem.isPresent() && await elem.isDisplayed(), isShown);
if (isShown) {
assert.isTrue(
await driver.find('.test-support-grist-nudge-card').isDisplayed()
);
} else {
assert.isFalse(await driver.find('.test-support-grist-nudge-card').isPresent());
assert.match(await elem.getAttribute('href'), /.*\/admin$/);
}
}
async function assertMenuHasSupportGrist(isShown: boolean) {
const elem = driver.find('.test-usermenu-support-grist');
assert.equal(await elem.isPresent() && await elem.isDisplayed(), isShown);
if (isShown) {
assert.equal(await elem.getAttribute('href'), sponsorshipUrl);
}
}

View File

@@ -393,17 +393,9 @@ describe('Telemetry', function() {
sandbox.restore();
});
it('GET /install/prefs returns 200 for non-default users', async function() {
it('GET /install/prefs returns 403 for non-default users', async function() {
const resp = await axios.get(`${homeUrl}/api/install/prefs`, kiwi);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {
telemetry: {
telemetryLevel: {
value: 'off',
source: 'preferences',
},
},
});
assert.equal(resp.status, 403);
});
it('GET /install/prefs returns 200 for the default user', async function() {