Merge branch 'master' into MM-50966-in-product-expansion
10
.github/workflows/channels-ci.yml
поставляемый
@@ -83,6 +83,16 @@ jobs:
|
|||||||
npm run mmjstool -- i18n clean-empty --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir --check
|
npm run mmjstool -- i18n clean-empty --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir --check
|
||||||
npm run mmjstool -- i18n check-empty-src --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
npm run mmjstool -- i18n check-empty-src --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
||||||
rm -rf tmp
|
rm -rf tmp
|
||||||
|
- name: ci/lint-boards
|
||||||
|
working-directory: webapp/boards
|
||||||
|
run: |
|
||||||
|
npm run i18n-extract
|
||||||
|
git --no-pager diff --exit-code i18n/en.json || (echo "Please run \"cd webapp/boards && npm run i18n-extract\" and commit the changes in webapp/boards/i18n/en.json." && exit 1)
|
||||||
|
- name: ci/lint-playbooks
|
||||||
|
working-directory: webapp/playbooks
|
||||||
|
run: |
|
||||||
|
npm run i18n-extract
|
||||||
|
git --no-pager diff --exit-code i18n/en.json || (echo "Please run \"cd webapp/playbooks && npm run i18n-extract\" and commit the changes in webapp/playbooks/i18n/en.json." && exit 1)
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
defaults:
|
defaults:
|
||||||
|
|||||||
@@ -5,3 +5,6 @@
|
|||||||
/webapp/package-lock.json @mattermost/web-platform
|
/webapp/package-lock.json @mattermost/web-platform
|
||||||
/webapp/platform/*/package.json @mattermost/web-platform
|
/webapp/platform/*/package.json @mattermost/web-platform
|
||||||
/webapp/scripts @mattermost/web-platform
|
/webapp/scripts @mattermost/web-platform
|
||||||
|
/server/channels/db/migrations @mattermost/server-platform
|
||||||
|
/server/boards/services/store/sqlstore/migrations @mattermost/server-platform
|
||||||
|
/server/playbooks/server/sqlstore/migrations @mattermost/server-platform
|
||||||
|
|||||||
@@ -98,6 +98,72 @@ describe('Create and delete board / card', () => {
|
|||||||
cy.findByText('for testing purposes only').should('be.visible');
|
cy.findByText('for testing purposes only').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('MM-T4276 Set up Board emoji', () => {
|
||||||
|
cy.visit('/boards');
|
||||||
|
|
||||||
|
// # Create an empty board and change tile to Testing
|
||||||
|
cy.findByText('Create an empty board').should('exist').click({force: true});
|
||||||
|
cy.get('.BoardComponent').should('exist');
|
||||||
|
|
||||||
|
// # Change Title
|
||||||
|
cy.findByPlaceholderText('Untitled board').should('be.visible').wait(timeouts.HALF_SEC);
|
||||||
|
|
||||||
|
// * Assert that the title is changed to "testing"
|
||||||
|
cy.findByPlaceholderText('Untitled board').
|
||||||
|
clear().
|
||||||
|
type('Testing').
|
||||||
|
type('{enter}').
|
||||||
|
should('have.value', 'Testing');
|
||||||
|
|
||||||
|
// # "Add icon" and "Show description" options appear
|
||||||
|
cy.findByText('Add icon').should('exist');
|
||||||
|
cy.findByText('show description').should('exist');
|
||||||
|
|
||||||
|
// # Click on "Add icon"
|
||||||
|
cy.findByText('Add icon').should('exist').click({force: true});
|
||||||
|
|
||||||
|
// * Assert that a random emoji is selected and added at the beginning of the board title
|
||||||
|
cy.get('.IconSelector').should('exist');
|
||||||
|
|
||||||
|
// # Click on the emoji next to the board title
|
||||||
|
cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true});
|
||||||
|
|
||||||
|
// * Assert that Dropdown menu with 3 options appears
|
||||||
|
cy.findByText('Random').should('exist');
|
||||||
|
cy.findByText('Pick icon').should('exist');
|
||||||
|
cy.findByText('Remove icon').should('exist');
|
||||||
|
|
||||||
|
// # Hover your mouse over the "Pick Icon" option
|
||||||
|
cy.findByText('Pick icon').trigger('mouseover');
|
||||||
|
|
||||||
|
// * Assert that emoji picker menu appears
|
||||||
|
cy.get('.IconSelector .menu-contents').should('exist');
|
||||||
|
|
||||||
|
// # Click on the emoji from the picker
|
||||||
|
cy.get('.EmojiPicker').should('exist').and('be.visible').within(() => {
|
||||||
|
// # Click on the emoji
|
||||||
|
cy.get("[aria-label='😀, grinning']").should('exist');
|
||||||
|
cy.get("[aria-label='😀, grinning']").eq(0).click({force: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
// * Assert that Selected emoji is now displayed next to the board title
|
||||||
|
cy.get('.IconSelector span').contains('😀');
|
||||||
|
|
||||||
|
// # Click on the emoji next to the board title
|
||||||
|
cy.get('.IconSelector .MenuWrapper').should('exist').click({force: true});
|
||||||
|
|
||||||
|
// * Assert that Dropdown menu with 3 options appears
|
||||||
|
cy.findByText('Random').should('exist');
|
||||||
|
cy.findByText('Pick icon').should('exist');
|
||||||
|
cy.findByText('Remove icon').should('exist');
|
||||||
|
|
||||||
|
// # Click "Remove icon"
|
||||||
|
cy.findByText('Remove icon').click({force: true});
|
||||||
|
|
||||||
|
// * Assert that Icon next to the board title is removed
|
||||||
|
cy.get('.IconSelector').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
it('MM-T5397 Can create and delete a board and a card', () => {
|
it('MM-T5397 Can create and delete a board and a card', () => {
|
||||||
// Visit a page and create new empty board
|
// Visit a page and create new empty board
|
||||||
cy.visit('/boards');
|
cy.visit('/boards');
|
||||||
|
|||||||
@@ -2,8 +2,21 @@
|
|||||||
|
|
||||||
#### 1. Start local server in a separate terminal.
|
#### 1. Start local server in a separate terminal.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Typically run the local server with:
|
||||||
|
cd server && make run
|
||||||
|
|
||||||
|
# Or build and distribute webapp including channels, boards and playbooks
|
||||||
|
# so that their product URLs do not rely on Webpack dev server.
|
||||||
|
# Especially important when running test inside the Playwright's docker container.
|
||||||
|
cd webapp && make dist
|
||||||
|
cd server && make run-server
|
||||||
|
```
|
||||||
|
|
||||||
#### 2. Install dependencies and run the test.
|
#### 2. Install dependencies and run the test.
|
||||||
|
|
||||||
|
Note: If you're using Node.js version 18 and above, you may need to set `NODE_OPTIONS='--no-experimental-fetch'`.
|
||||||
|
|
||||||
```
|
```
|
||||||
# Install npm packages
|
# Install npm packages
|
||||||
npm i
|
npm i
|
||||||
@@ -32,14 +45,16 @@ npm run test
|
|||||||
Change to root directory, run docker container
|
Change to root directory, run docker container
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.30.0-focal /bin/bash
|
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.32.0-focal /bin/bash
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Inside the docker container
|
#### 2. Inside the docker container
|
||||||
|
|
||||||
```
|
```
|
||||||
|
export NODE_OPTIONS='--no-experimental-fetch'
|
||||||
export PW_BASE_URL=http://host.docker.internal:8065
|
export PW_BASE_URL=http://host.docker.internal:8065
|
||||||
cd mattermost/e2e/playwright
|
export PW_HEADLESS=true
|
||||||
|
cd mattermost/e2e-tests/playwright
|
||||||
|
|
||||||
# Install npm packages. Use "npm ci" to match the automated environment
|
# Install npm packages. Use "npm ci" to match the automated environment
|
||||||
npm ci
|
npm ci
|
||||||
|
|||||||
991
e2e-tests/playwright/package-lock.json
сгенерированный
@@ -1,33 +1,35 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "PW_SNAPSHOT_ENABLE=true playwright test",
|
"test": "cross-env PW_SNAPSHOT_ENABLE=true playwright test",
|
||||||
"percy": "PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=iphone --project=ipad",
|
"percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=iphone --project=ipad",
|
||||||
"tsc": "tsc -b",
|
"tsc": "tsc -b",
|
||||||
"lint": "eslint . --ext .js,.ts",
|
"lint": "eslint . --ext .js,.ts",
|
||||||
"prettier": "prettier --write .",
|
"prettier": "prettier --write .",
|
||||||
"check": "npm run tsc && npm run lint && npm run prettier",
|
"check": "npm run tsc && npm run lint && npm run prettier",
|
||||||
"codegen": "playwright codegen $PW_BASE_URL",
|
"codegen": "cross-env playwright codegen $PW_BASE_URL",
|
||||||
"test-slomo": "PW_SNAPSHOT_ENABLE=true PW_HEADLESS=false PW_SLOWMO=1000 playwright test",
|
"playwright-ui": "playwright test --ui",
|
||||||
|
"test-slomo": "cross-env PW_SNAPSHOT_ENABLE=true PW_SLOWMO=1000 playwright test",
|
||||||
"show-report": "npx playwright show-report"
|
"show-report": "npx playwright show-report"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@percy/cli": "1.18.0",
|
"@percy/cli": "1.23.0",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.32.3",
|
"@playwright/test": "1.32.3",
|
||||||
"async-wait-until": "2.0.12",
|
"async-wait-until": "2.0.12",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"deepmerge": "4.3.0",
|
"deepmerge": "4.3.1",
|
||||||
"dotenv": "16.0.3",
|
"dotenv": "16.0.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"isomorphic-unfetch": "4.0.2",
|
"isomorphic-unfetch": "4.0.2",
|
||||||
"uuid": "9.0.0"
|
"uuid": "9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/uuid": "9.0.0",
|
"@types/uuid": "9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
"@typescript-eslint/eslint-plugin": "5.59.0",
|
||||||
"@typescript-eslint/parser": "5.51.0",
|
"@typescript-eslint/parser": "5.59.0",
|
||||||
"eslint": "8.34.0",
|
"cross-env": "7.0.3",
|
||||||
"prettier": "2.8.4",
|
"eslint": "8.38.0",
|
||||||
"typescript": "4.9.5"
|
"prettier": "2.8.7",
|
||||||
|
"typescript": "5.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
# - Default to "false" if not set.
|
# - Default to "false" if not set.
|
||||||
|
|
||||||
# 12. PW_HEADLESS
|
# 12. PW_HEADLESS
|
||||||
# - Default to "true" if not set. Set to false to run test in head mode.
|
# - Default to "false" or headless mode if not set. Set to true to run test in headed mode.
|
||||||
|
|
||||||
# 13. PW_SLOWMO
|
# 13. PW_SLOWMO
|
||||||
# - Default to "0" if not set which means normal test speed run. Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
|
# - Default to "0" if not set which means normal test speed run. Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
|
||||||
|
|||||||
@@ -3,16 +3,18 @@
|
|||||||
|
|
||||||
import {writeFile} from 'node:fs/promises';
|
import {writeFile} from 'node:fs/promises';
|
||||||
|
|
||||||
import {request, Browser} from '@playwright/test';
|
import {request, Browser, BrowserContext} from '@playwright/test';
|
||||||
|
|
||||||
import {UserProfile} from '@mattermost/types/users';
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
import testConfig from '@e2e-test.config';
|
import testConfig from '@e2e-test.config';
|
||||||
|
|
||||||
export class TestBrowser {
|
export class TestBrowser {
|
||||||
readonly browser: Browser;
|
readonly browser: Browser;
|
||||||
|
context: BrowserContext | null;
|
||||||
|
|
||||||
constructor(browser: Browser) {
|
constructor(browser: Browser) {
|
||||||
this.browser = browser;
|
this.browser = browser;
|
||||||
|
this.context = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(user: UserProfile | null) {
|
async login(user: UserProfile | null) {
|
||||||
@@ -27,8 +29,16 @@ export class TestBrowser {
|
|||||||
const context = await this.browser.newContext(options);
|
const context = await this.browser.newContext(options);
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
return {context, page};
|
return {context, page};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
if (this.context) {
|
||||||
|
await this.context.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginByAPI(loginId: string, password: string, token = '', ldapOnly = false) {
|
export async function loginByAPI(loginId: string, password: string, token = '', ldapOnly = false) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
// This is based on "packages/client/src/client4.ts". Modified for node client.
|
// This is based on "webapp/platform/client/src/client4.ts". Modified for node client.
|
||||||
// Update should be made in comparison with the base Client4.
|
// Update should be made in comparison with the base Client4.
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@@ -134,7 +134,7 @@ export default class Client extends Client4 {
|
|||||||
|
|
||||||
// *****************************************************************************
|
// *****************************************************************************
|
||||||
// Boards client
|
// Boards client
|
||||||
// based on https://github.com/mattermost/focalboard/blob/main/webapp/src/octoClient.ts
|
// based on "webapp/boards/src/octoClient.ts"
|
||||||
// *****************************************************************************
|
// *****************************************************************************
|
||||||
|
|
||||||
async patchUserConfig(userID: string, patch: UserConfigPatch): Promise<UserPreference[] | undefined> {
|
async patchUserConfig(userID: string, patch: UserConfigPatch): Promise<UserPreference[] | undefined> {
|
||||||
|
|||||||
@@ -318,7 +318,6 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
LoginButtonColor: '#0000',
|
LoginButtonColor: '#0000',
|
||||||
LoginButtonBorderColor: '#2389D7',
|
LoginButtonBorderColor: '#2389D7',
|
||||||
LoginButtonTextColor: '#2389D7',
|
LoginButtonTextColor: '#2389D7',
|
||||||
EnableInactivityEmail: true,
|
|
||||||
},
|
},
|
||||||
RateLimitSettings: {
|
RateLimitSettings: {
|
||||||
Enable: false,
|
Enable: false,
|
||||||
@@ -532,6 +531,7 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
EnableRemoteClusterService: false,
|
EnableRemoteClusterService: false,
|
||||||
EnableAppBar: false,
|
EnableAppBar: false,
|
||||||
PatchPluginsReactDOM: false,
|
PatchPluginsReactDOM: false,
|
||||||
|
DisableRefetchingOnBrowserFocus: false,
|
||||||
},
|
},
|
||||||
AnalyticsSettings: {
|
AnalyticsSettings: {
|
||||||
MaxUsersForStatistics: 2500,
|
MaxUsersForStatistics: 2500,
|
||||||
@@ -621,12 +621,6 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
'com.mattermost.nps': {
|
'com.mattermost.nps': {
|
||||||
Enable: true,
|
Enable: true,
|
||||||
},
|
},
|
||||||
focalboard: {
|
|
||||||
Enable: true,
|
|
||||||
},
|
|
||||||
playbooks: {
|
|
||||||
Enable: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
EnableMarketplace: true,
|
EnableMarketplace: true,
|
||||||
EnableRemoteMarketplace: true,
|
EnableRemoteMarketplace: true,
|
||||||
@@ -670,13 +664,11 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
BoardsFeatureFlags: '',
|
BoardsFeatureFlags: '',
|
||||||
BoardsDataRetention: false,
|
BoardsDataRetention: false,
|
||||||
NormalizeLdapDNs: false,
|
NormalizeLdapDNs: false,
|
||||||
EnableInactivityCheckJob: true,
|
|
||||||
UseCaseOnboarding: true,
|
|
||||||
GraphQL: false,
|
GraphQL: false,
|
||||||
InsightsEnabled: true,
|
InsightsEnabled: true,
|
||||||
CommandPalette: false,
|
CommandPalette: false,
|
||||||
SendWelcomePost: true,
|
SendWelcomePost: true,
|
||||||
WorkTemplate: false,
|
WorkTemplate: true,
|
||||||
PostPriority: true,
|
PostPriority: true,
|
||||||
WysiwygEditor: false,
|
WysiwygEditor: false,
|
||||||
PeopleProduct: false,
|
PeopleProduct: false,
|
||||||
@@ -685,7 +677,9 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
ThreadsEverywhere: false,
|
ThreadsEverywhere: false,
|
||||||
GlobalDrafts: true,
|
GlobalDrafts: true,
|
||||||
OnboardingTourTips: true,
|
OnboardingTourTips: true,
|
||||||
|
DeprecateCloudFree: false,
|
||||||
AppsSidebarCategory: false,
|
AppsSidebarCategory: false,
|
||||||
|
CloudReverseTrial: false,
|
||||||
},
|
},
|
||||||
ImportSettings: {
|
ImportSettings: {
|
||||||
Directory: './import',
|
Directory: './import',
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
import {ClientError} from '@mattermost/client/client4';
|
||||||
import {PreferenceType} from '@mattermost/types/preferences';
|
import {PreferenceType} from '@mattermost/types/preferences';
|
||||||
import testConfig from '@e2e-test.config';
|
import testConfig from '@e2e-test.config';
|
||||||
|
|
||||||
@@ -77,10 +79,21 @@ export async function initSetup({
|
|||||||
offTopicUrl: getUrl(team.name, 'off-topic'),
|
offTopicUrl: getUrl(team.name, 'off-topic'),
|
||||||
townSquareUrl: getUrl(team.name, 'town-square'),
|
townSquareUrl: getUrl(team.name, 'town-square'),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
// log an error for debugging
|
// log an error for debugging
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(err);
|
const err = error as ClientError;
|
||||||
|
if (err.message === 'Could not parse multipart form.') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(chalk.yellow(`node version: ${process.version}\nNODE_OPTIONS: ${process.env.NODE_OPTIONS}`));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
chalk.green(
|
||||||
|
`This failed due to the experimental fetch support in Node.js starting v18.0.0.\nYou may set environment variable: "export NODE_OPTIONS='--no-experimental-fetch'", then try again.'`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
expect(err, 'Should not throw an error').toBeFalsy();
|
expect(err, 'Should not throw an error').toBeFalsy();
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const test = base.extend<ExtendedFixtures>({
|
|||||||
pw: async ({browser}, use) => {
|
pw: async ({browser}, use) => {
|
||||||
const pw = new PlaywrightExtended(browser);
|
const pw = new PlaywrightExtended(browser);
|
||||||
await use(pw);
|
await use(pw);
|
||||||
|
await pw.testBrowser.close();
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
pages: async ({}, use) => {
|
pages: async ({}, use) => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default class GlobalHeader {
|
|||||||
|
|
||||||
async switchProduct(name: string) {
|
async switchProduct(name: string) {
|
||||||
await this.productSwitchMenu.click();
|
await this.productSwitchMenu.click();
|
||||||
await this.container.getByRole('link', {name: ` ${name}`}).click();
|
await this.container.getByRole('link', {name}).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async toBeVisible(name: string) {
|
async toBeVisible(name: string) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const config: TestConfig = {
|
|||||||
// CI
|
// CI
|
||||||
isCI: !!process.env.CI,
|
isCI: !!process.env.CI,
|
||||||
// Playwright
|
// Playwright
|
||||||
headless: parseBool(process.env.PW_HEADLESS, false),
|
headless: parseBool(process.env.PW_HEADLESS, true),
|
||||||
slowMo: parseNumber(process.env.PW_SLOWMO, 0),
|
slowMo: parseNumber(process.env.PW_SLOWMO, 0),
|
||||||
workers: parseNumber(process.env.PW_WORKERS, 1),
|
workers: parseNumber(process.env.PW_WORKERS, 1),
|
||||||
// Visual tests
|
// Visual tests
|
||||||
|
|||||||
|
До Ширина: | Высота: | Размер: 129 KiB После Ширина: | Высота: | Размер: 127 KiB |
|
До Ширина: | Высота: | Размер: 184 KiB После Ширина: | Высота: | Размер: 182 KiB |
|
До Ширина: | Высота: | Размер: 246 KiB После Ширина: | Высота: | Размер: 238 KiB |
|
До Ширина: | Высота: | Размер: 38 KiB После Ширина: | Высота: | Размер: 38 KiB |
|
До Ширина: | Высота: | Размер: 65 KiB После Ширина: | Высота: | Размер: 63 KiB |
|
До Ширина: | Высота: | Размер: 85 KiB После Ширина: | Высота: | Размер: 84 KiB |
|
До Ширина: | Высота: | Размер: 77 KiB После Ширина: | Высота: | Размер: 71 KiB |
|
До Ширина: | Высота: | Размер: 113 KiB После Ширина: | Высота: | Размер: 106 KiB |
|
До Ширина: | Высота: | Размер: 183 KiB После Ширина: | Высота: | Размер: 167 KiB |
|
До Ширина: | Высота: | Размер: 178 KiB После Ширина: | Высота: | Размер: 148 KiB |
|
До Ширина: | Высота: | Размер: 105 KiB После Ширина: | Высота: | Размер: 105 KiB |
|
До Ширина: | Высота: | Размер: 169 KiB После Ширина: | Высота: | Размер: 170 KiB |
|
До Ширина: | Высота: | Размер: 212 KiB После Ширина: | Высота: | Размер: 213 KiB |
|
До Ширина: | Высота: | Размер: 157 KiB После Ширина: | Высота: | Размер: 156 KiB |
|
До Ширина: | Высота: | Размер: 150 KiB После Ширина: | Высота: | Размер: 150 KiB |
|
До Ширина: | Высота: | Размер: 147 KiB После Ширина: | Высота: | Размер: 147 KiB |
|
До Ширина: | Высота: | Размер: 270 KiB После Ширина: | Высота: | Размер: 271 KiB |
|
До Ширина: | Высота: | Размер: 312 KiB После Ширина: | Высота: | Размер: 312 KiB |
|
До Ширина: | Высота: | Размер: 241 KiB После Ширина: | Высота: | Размер: 241 KiB |
|
До Ширина: | Высота: | Размер: 276 KiB После Ширина: | Высота: | Размер: 277 KiB |
|
До Ширина: | Высота: | Размер: 297 KiB После Ширина: | Высота: | Размер: 297 KiB |
|
До Ширина: | Высота: | Размер: 230 KiB После Ширина: | Высота: | Размер: 230 KiB |
|
До Ширина: | Высота: | Размер: 156 KiB После Ширина: | Высота: | Размер: 165 KiB |
|
До Ширина: | Высота: | Размер: 160 KiB После Ширина: | Высота: | Размер: 170 KiB |
|
До Ширина: | Высота: | Размер: 285 KiB После Ширина: | Высота: | Размер: 296 KiB |
|
До Ширина: | Высота: | Размер: 368 KiB После Ширина: | Высота: | Размер: 411 KiB |
|
До Ширина: | Высота: | Размер: 276 KiB После Ширина: | Высота: | Размер: 355 KiB |
|
До Ширина: | Высота: | Размер: 280 KiB После Ширина: | Высота: | Размер: 291 KiB |
|
До Ширина: | Высота: | Размер: 348 KiB После Ширина: | Высота: | Размер: 393 KiB |
|
До Ширина: | Высота: | Размер: 256 KiB После Ширина: | Высота: | Размер: 266 KiB |
@@ -138,7 +138,7 @@ TEMPLATES_DIR=templates
|
|||||||
|
|
||||||
# Plugins Packages
|
# Plugins Packages
|
||||||
PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2
|
PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.2.2
|
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.4.0
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.15.1
|
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.15.1
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0
|
PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) {
|
|||||||
}
|
}
|
||||||
*settings.DriverName = s.dbType
|
*settings.DriverName = s.dbType
|
||||||
|
|
||||||
db := sqlstore.SetupConnection("master", connectionString, &settings)
|
db, err := sqlstore.SetupConnection("master", connectionString, &settings, sqlstore.DBPingAttempts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|||||||
ENV PATH="/mattermost/bin:${PATH}"
|
ENV PATH="/mattermost/bin:${PATH}"
|
||||||
ARG PUID=2000
|
ARG PUID=2000
|
||||||
ARG PGID=2000
|
ARG PGID=2000
|
||||||
ARG MM_PACKAGE="https://releases.mattermost.com/7.9.2/mattermost-7.9.2-linux-amd64.tar.gz?src=docker"
|
ARG MM_PACKAGE="https://releases.mattermost.com/7.10.0/mattermost-7.10.0-linux-amd64.tar.gz?src=docker"
|
||||||
|
|
||||||
# # Install needed packages and indirect dependencies
|
# # Install needed packages and indirect dependencies
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
|||||||
@@ -323,7 +323,14 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
defer c.LogAuditRec(auditRec)
|
defer c.LogAuditRec(auditRec)
|
||||||
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
|
audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs)
|
||||||
|
|
||||||
// checks that user is a member of the specified channel, and that they have permission to use slash commands in it
|
// Checks that user is a member of the specified channel, and that they have permission to create a post in it.
|
||||||
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionCreatePost) {
|
||||||
|
c.SetPermissionError(model.PermissionCreatePost)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For compatibility reasons, PermissionCreatePost is also checked.
|
||||||
|
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) {
|
||||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||||
return
|
return
|
||||||
@@ -343,6 +350,13 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
|
// if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that
|
||||||
// they can't just execute slash commands against arbitrary teams
|
// they can't just execute slash commands against arbitrary teams
|
||||||
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
|
if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil {
|
||||||
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreatePost) {
|
||||||
|
c.SetPermissionError(model.PermissionCreatePost)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For compatibility reasons, PermissionCreatePost is also checked.
|
||||||
|
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) {
|
||||||
c.SetPermissionError(model.PermissionUseSlashCommands)
|
c.SetPermissionError(model.PermissionUseSlashCommands)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||||
"github.com/mattermost/mattermost-server/server/v8/model"
|
"github.com/mattermost/mattermost-server/server/v8/model"
|
||||||
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
"github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog"
|
||||||
)
|
)
|
||||||
@@ -1065,3 +1066,80 @@ func TestExecuteCommandInTeamUserIsNotOn(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
CheckForbiddenStatus(t, resp)
|
CheckForbiddenStatus(t, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecuteCommandReadOnly(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
ctx := request.EmptyContext(th.TestLogger)
|
||||||
|
defer th.TearDown()
|
||||||
|
client := th.Client
|
||||||
|
|
||||||
|
enableCommands := *th.App.Config().ServiceSettings.EnableCommands
|
||||||
|
allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
|
||||||
|
defer func() {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
||||||
|
})
|
||||||
|
|
||||||
|
expectedCommandResponse := &model.CommandResponse{
|
||||||
|
Text: "test post command response",
|
||||||
|
ResponseType: model.CommandResponseTypeInChannel,
|
||||||
|
Type: "custom_test",
|
||||||
|
Props: map[string]any{"someprop": "somevalue"},
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, http.MethodPost, r.Method)
|
||||||
|
r.ParseForm()
|
||||||
|
require.Equal(t, th.BasicTeam.Name, r.FormValue("team_domain"))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(expectedCommandResponse); err != nil {
|
||||||
|
th.TestLogger.Warn("Error while writing response", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
// create a slash command on that team
|
||||||
|
postCmd := &model.Command{
|
||||||
|
CreatorId: th.BasicUser.Id,
|
||||||
|
TeamId: th.BasicTeam.Id,
|
||||||
|
URL: ts.URL,
|
||||||
|
Method: model.CommandMethodPost,
|
||||||
|
Trigger: "postcommand",
|
||||||
|
}
|
||||||
|
_, appErr := th.App.CreateCommand(postCmd)
|
||||||
|
require.Nil(t, appErr, "failed to create post command")
|
||||||
|
|
||||||
|
// Confirm that the command works when the channel is not read only
|
||||||
|
_, resp, err := client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand")
|
||||||
|
require.NoError(t, err)
|
||||||
|
CheckOKStatus(t, resp)
|
||||||
|
|
||||||
|
// Enable Enterprise features
|
||||||
|
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||||
|
|
||||||
|
th.App.SetPhase2PermissionsMigrationStatus(true)
|
||||||
|
|
||||||
|
_, appErr = th.App.PatchChannelModerationsForChannel(
|
||||||
|
ctx,
|
||||||
|
th.BasicChannel,
|
||||||
|
[]*model.ChannelModerationPatch{{
|
||||||
|
Name: &model.PermissionCreatePost.Id,
|
||||||
|
Roles: &model.ChannelModeratedRolesPatch{
|
||||||
|
Guests: model.NewBool(false),
|
||||||
|
Members: model.NewBool(false),
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
|
||||||
|
// Confirm that the command fails when the channel is read only
|
||||||
|
_, resp, err = client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand")
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
}
|
||||||
|
|||||||
@@ -892,6 +892,7 @@ func TestCompleteOnboarding(t *testing.T) {
|
|||||||
|
|
||||||
req := &model.CompleteOnboardingRequest{
|
req := &model.CompleteOnboardingRequest{
|
||||||
InstallPlugins: []string{"testplugin2"},
|
InstallPlugins: []string{"testplugin2"},
|
||||||
|
Organization: "my-org",
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("as a regular user", func(t *testing.T) {
|
t.Run("as a regular user", func(t *testing.T) {
|
||||||
|
|||||||
@@ -3106,6 +3106,10 @@ func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
||||||
|
c.SetPermissionError(model.PermissionReadChannel)
|
||||||
|
return
|
||||||
|
}
|
||||||
extendedStr := r.URL.Query().Get("extended")
|
extendedStr := r.URL.Query().Get("extended")
|
||||||
extended, _ := strconv.ParseBool(extendedStr)
|
extended, _ := strconv.ParseBool(extendedStr)
|
||||||
|
|
||||||
@@ -3136,6 +3140,10 @@ func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||||
|
c.SetPermissionError(model.PermissionViewTeam)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
options := model.GetUserThreadsOpts{
|
options := model.GetUserThreadsOpts{
|
||||||
Since: 0,
|
Since: 0,
|
||||||
@@ -3213,6 +3221,10 @@ func updateReadStateThreadByUser(c *Context, w http.ResponseWriter, r *http.Requ
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
||||||
|
c.SetPermissionError(model.PermissionReadChannel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
|
thread, err := c.App.UpdateThreadReadForUser(c.AppContext, c.AppContext.Session().Id, c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, c.Params.Timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3279,6 +3291,10 @@ func unfollowThreadByUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.ThreadId, model.PermissionReadChannel) {
|
||||||
|
c.SetPermissionError(model.PermissionReadChannel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
|
err := c.App.UpdateThreadFollowForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3338,6 +3354,10 @@ func updateReadStateAllThreadsByUser(c *Context, w http.ResponseWriter, r *http.
|
|||||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
||||||
|
c.SetPermissionError(model.PermissionViewTeam)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
|
err := c.App.UpdateThreadsReadForUser(c.Params.UserId, c.Params.TeamId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6360,6 +6360,15 @@ func TestGetThreadsForUser(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uss.TotalUnreadThreads, int64(2))
|
require.Equal(t, uss.TotalUnreadThreads, int64(2))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should error when not a team member", func(t *testing.T) {
|
||||||
|
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||||
|
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
||||||
|
|
||||||
|
_, resp, err := th.Client.GetUserThreads(th.BasicUser.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{})
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestThreadSocketEvents(t *testing.T) {
|
func TestThreadSocketEvents(t *testing.T) {
|
||||||
@@ -6855,52 +6864,64 @@ func TestSingleThreadGet(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
client := th.Client
|
client := th.Client
|
||||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
|
|
||||||
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id)
|
|
||||||
|
|
||||||
// create a post by regular user
|
t.Run("get single thread", func(t *testing.T) {
|
||||||
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
|
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.BasicUser.Id)
|
||||||
// reply with another
|
defer th.App.Srv().Store().Post().PermanentDeleteByUser(th.SystemAdminUser.Id)
|
||||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
|
|
||||||
|
|
||||||
// create another thread to check that we are not returning it by mistake
|
// create a post by regular user
|
||||||
rpost2, _ := postAndCheck(t, client, &model.Post{
|
rpost, _ := postAndCheck(t, client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
|
||||||
ChannelId: th.BasicChannel2.Id,
|
// reply with another
|
||||||
Message: "testMsg2",
|
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
|
||||||
Metadata: &model.PostMetadata{
|
|
||||||
Priority: &model.PostPriority{
|
// create another thread to check that we are not returning it by mistake
|
||||||
Priority: model.NewString(model.PostPriorityUrgent),
|
rpost2, _ := postAndCheck(t, client, &model.Post{
|
||||||
|
ChannelId: th.BasicChannel2.Id,
|
||||||
|
Message: "testMsg2",
|
||||||
|
Metadata: &model.PostMetadata{
|
||||||
|
Priority: &model.PostPriority{
|
||||||
|
Priority: model.NewString(model.PostPriorityUrgent),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id})
|
||||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id})
|
|
||||||
|
|
||||||
// regular user should have two threads with 3 replies total
|
// regular user should have two threads with 3 replies total
|
||||||
threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil)
|
threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil)
|
||||||
|
|
||||||
tr, _, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false)
|
tr, _, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, tr)
|
require.NotNil(t, tr)
|
||||||
require.Equal(t, threads.Threads[0].PostId, tr.PostId)
|
require.Equal(t, threads.Threads[0].PostId, tr.PostId)
|
||||||
require.Empty(t, tr.Participants[0].Username)
|
require.Empty(t, tr.Participants[0].Username)
|
||||||
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.ServiceSettings.PostPriority = false
|
*cfg.ServiceSettings.PostPriority = false
|
||||||
|
})
|
||||||
|
|
||||||
|
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, tr.Participants[0].Username)
|
||||||
|
require.Equal(t, false, tr.IsUrgent)
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.PostPriority = true
|
||||||
|
cfg.FeatureFlags.PostPriority = true
|
||||||
|
})
|
||||||
|
|
||||||
|
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, true, tr.IsUrgent)
|
||||||
})
|
})
|
||||||
|
|
||||||
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
t.Run("should error when not a team member", func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||||
require.NotEmpty(t, tr.Participants[0].Username)
|
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
||||||
require.Equal(t, false, tr.IsUrgent)
|
|
||||||
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
_, resp, err := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), false)
|
||||||
*cfg.ServiceSettings.PostPriority = true
|
require.Error(t, err)
|
||||||
cfg.FeatureFlags.PostPriority = true
|
CheckForbiddenStatus(t, resp)
|
||||||
})
|
})
|
||||||
|
|
||||||
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, true, tr.IsUrgent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMaintainUnreadMentionsInThread(t *testing.T) {
|
func TestMaintainUnreadMentionsInThread(t *testing.T) {
|
||||||
@@ -7072,6 +7093,23 @@ func TestReadThreads(t *testing.T) {
|
|||||||
|
|
||||||
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil)
|
checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 1, 1, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should error when not a team member", func(t *testing.T) {
|
||||||
|
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
|
||||||
|
defer th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
|
||||||
|
|
||||||
|
_, resp, err := th.Client.UpdateThreadReadForUser(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.GetMillis())
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
|
||||||
|
_, resp, err = th.Client.SetThreadUnreadByPostId(th.BasicUser.Id, th.BasicTeam.Id, model.NewId(), model.NewId())
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
|
||||||
|
resp, err = th.Client.UpdateThreadsReadForUser(th.BasicUser.Id, th.BasicTeam.Id)
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarkThreadUnreadMentionCount(t *testing.T) {
|
func TestMarkThreadUnreadMentionCount(t *testing.T) {
|
||||||
|
|||||||
@@ -2518,6 +2518,9 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove
|
|||||||
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
|
if err := a.Srv().Store().ChannelMemberHistory().LogLeaveEvent(userIDToRemove, channel.Id, model.GetMillis()); err != nil {
|
||||||
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return model.NewAppError("removeUserFromChannel", "app.channel_member_history.log_leave_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
if err := a.Srv().Store().Thread().DeleteMembershipsForChannel(userIDToRemove, channel.Id); err != nil {
|
||||||
|
return model.NewAppError("removeUserFromChannel", model.NoTranslation, nil, "failed to delete threadmemberships upon leaving channel", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
if isGuest {
|
if isGuest {
|
||||||
currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove)
|
currentMembers, err := a.GetChannelMembersForUser(c, channel.TeamId, userIDToRemove)
|
||||||
|
|||||||
@@ -609,6 +609,85 @@ func TestLeaveDefaultChannel(t *testing.T) {
|
|||||||
_, err = th.App.GetChannelMember(th.Context, townSquare.Id, guest.Id)
|
_, err = th.App.GetChannelMember(th.Context, townSquare.Id, guest.Id)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Trying to leave the default channel should not delete thread memberships", func(t *testing.T) {
|
||||||
|
post := &model.Post{
|
||||||
|
ChannelId: townSquare.Id,
|
||||||
|
Message: "root post",
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
}
|
||||||
|
rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
reply := &model.Post{
|
||||||
|
ChannelId: townSquare.Id,
|
||||||
|
Message: "reply post",
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
RootId: rpost.Id,
|
||||||
|
}
|
||||||
|
_, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, threads.Threads, 1)
|
||||||
|
|
||||||
|
err = th.App.LeaveChannel(th.Context, townSquare.Id, th.BasicUser.Id)
|
||||||
|
assert.NotNil(t, err, "It should fail to remove a regular user from the default channel")
|
||||||
|
assert.Equal(t, err.Id, "api.channel.remove.default.app_error")
|
||||||
|
|
||||||
|
threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, townSquare.TeamId, model.GetUserThreadsOpts{})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, threads.Threads, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLeaveChannel(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
createThread := func(channel *model.Channel) (rpost *model.Post) {
|
||||||
|
t.Helper()
|
||||||
|
post := &model.Post{
|
||||||
|
ChannelId: channel.Id,
|
||||||
|
Message: "root post",
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
reply := &model.Post{
|
||||||
|
ChannelId: channel.Id,
|
||||||
|
Message: "reply post",
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
RootId: rpost.Id,
|
||||||
|
}
|
||||||
|
_, err = th.App.CreatePost(th.Context, reply, th.BasicChannel, false, true)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
return rpost
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("thread memberships are deleted", func(t *testing.T) {
|
||||||
|
createThread(th.BasicChannel)
|
||||||
|
channel2 := th.createChannel(th.Context, th.BasicTeam, model.ChannelTypeOpen)
|
||||||
|
createThread(channel2)
|
||||||
|
|
||||||
|
threads, err := th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, threads.Threads, 2)
|
||||||
|
|
||||||
|
err = th.App.LeaveChannel(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
_, err = th.App.GetChannelMember(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
|
||||||
|
require.NotNil(t, err, "It should remove channel membership")
|
||||||
|
|
||||||
|
threads, err = th.App.GetThreadsForUser(th.BasicUser.Id, th.BasicChannel.TeamId, model.GetUserThreadsOpts{})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Len(t, threads.Threads, 1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLeaveLastChannel(t *testing.T) {
|
func TestLeaveLastChannel(t *testing.T) {
|
||||||
|
|||||||
@@ -28,6 +28,24 @@ func (a *App) markAdminOnboardingComplete(c *request.Context) *model.AppError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
|
func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnboardingRequest) *model.AppError {
|
||||||
|
isCloud := a.Srv().License() != nil && *a.Srv().License().Features.Cloud
|
||||||
|
|
||||||
|
if !isCloud && request.Organization == "" {
|
||||||
|
mlog.Error("No organization name provided for self hosted onboarding")
|
||||||
|
return model.NewAppError("CompleteOnboarding", "api.error_no_organization_name_provided_for_self_hosted_onboarding", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Organization != "" {
|
||||||
|
err := a.Srv().Store().System().SaveOrUpdate(&model.System{
|
||||||
|
Name: model.SystemOrganizationName,
|
||||||
|
Value: request.Organization,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// don't block onboarding because of that.
|
||||||
|
a.Log().Error("failed to save organization name", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
|
pluginsEnvironment := a.Channels().GetPluginsEnvironment()
|
||||||
if pluginsEnvironment == nil {
|
if pluginsEnvironment == nil {
|
||||||
return a.markAdminOnboardingComplete(c)
|
return a.markAdminOnboardingComplete(c)
|
||||||
|
|||||||
30
server/channels/app/onboarding_test.go
Обычный файл
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
|
||||||
|
mm_model "github.com/mattermost/mattermost-server/server/v8/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOnboardingSavesOrganizationName(t *testing.T) {
|
||||||
|
th := Setup(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
err := th.App.CompleteOnboarding(&request.Context{}, &mm_model.CompleteOnboardingRequest{
|
||||||
|
Organization: "Mattermost In Tests",
|
||||||
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer func() {
|
||||||
|
th.App.Srv().Store().System().PermanentDeleteByName(mm_model.SystemOrganizationName)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sys, storeErr := th.App.Srv().Store().System().GetByName(mm_model.SystemOrganizationName)
|
||||||
|
require.NoError(t, storeErr)
|
||||||
|
require.Equal(t, "Mattermost In Tests", sys.Value)
|
||||||
|
}
|
||||||
@@ -212,6 +212,8 @@ channels/db/migrations/mysql/000105_remove_tokens.down.sql
|
|||||||
channels/db/migrations/mysql/000105_remove_tokens.up.sql
|
channels/db/migrations/mysql/000105_remove_tokens.up.sql
|
||||||
channels/db/migrations/mysql/000106_fileinfo_channelid.down.sql
|
channels/db/migrations/mysql/000106_fileinfo_channelid.down.sql
|
||||||
channels/db/migrations/mysql/000106_fileinfo_channelid.up.sql
|
channels/db/migrations/mysql/000106_fileinfo_channelid.up.sql
|
||||||
|
channels/db/migrations/mysql/000107_threadmemberships_cleanup.down.sql
|
||||||
|
channels/db/migrations/mysql/000107_threadmemberships_cleanup.up.sql
|
||||||
channels/db/migrations/postgres/000001_create_teams.down.sql
|
channels/db/migrations/postgres/000001_create_teams.down.sql
|
||||||
channels/db/migrations/postgres/000001_create_teams.up.sql
|
channels/db/migrations/postgres/000001_create_teams.up.sql
|
||||||
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
channels/db/migrations/postgres/000002_create_team_members.down.sql
|
||||||
@@ -424,3 +426,5 @@ channels/db/migrations/postgres/000105_remove_tokens.down.sql
|
|||||||
channels/db/migrations/postgres/000105_remove_tokens.up.sql
|
channels/db/migrations/postgres/000105_remove_tokens.up.sql
|
||||||
channels/db/migrations/postgres/000106_fileinfo_channelid.down.sql
|
channels/db/migrations/postgres/000106_fileinfo_channelid.down.sql
|
||||||
channels/db/migrations/postgres/000106_fileinfo_channelid.up.sql
|
channels/db/migrations/postgres/000106_fileinfo_channelid.up.sql
|
||||||
|
channels/db/migrations/postgres/000107_threadmemberships_cleanup.down.sql
|
||||||
|
channels/db/migrations/postgres/000107_threadmemberships_cleanup.up.sql
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Skipping it because the forward migrations are destructive
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
DELETE FROM
|
||||||
|
tm USING ThreadMemberships AS tm
|
||||||
|
JOIN Threads ON Threads.PostId = tm.PostId
|
||||||
|
WHERE
|
||||||
|
(tm.UserId, Threads.ChannelId) NOT IN (SELECT UserId, ChannelId FROM ChannelMembers);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Skipping it because the forward migrations are destructive
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
DELETE FROM threadmemberships WHERE (postid, userid) IN (
|
||||||
|
SELECT
|
||||||
|
threadmemberships.postid,
|
||||||
|
threadmemberships.userid
|
||||||
|
FROM
|
||||||
|
threadmemberships
|
||||||
|
JOIN threads ON threads.postid = threadmemberships.postid
|
||||||
|
LEFT JOIN channelmembers ON channelmembers.userid = threadmemberships.userid
|
||||||
|
AND threads.channelid = channelmembers.channelid
|
||||||
|
WHERE
|
||||||
|
channelmembers.channelid IS NULL
|
||||||
|
);
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
type MetricsInterface interface {
|
type MetricsInterface interface {
|
||||||
Register()
|
Register()
|
||||||
RegisterDBCollector(db *sql.DB, name string)
|
RegisterDBCollector(db *sql.DB, name string)
|
||||||
|
UnregisterDBCollector(db *sql.DB, name string)
|
||||||
|
|
||||||
IncrementPostCreate()
|
IncrementPostCreate()
|
||||||
IncrementWebhookPost()
|
IncrementWebhookPost()
|
||||||
|
|||||||
@@ -319,6 +319,11 @@ func (_m *MetricsInterface) SetReplicaLagTime(node string, value float64) {
|
|||||||
_m.Called(node, value)
|
_m.Called(node, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnregisterDBCollector provides a mock function with given fields: db, name
|
||||||
|
func (_m *MetricsInterface) UnregisterDBCollector(db *sql.DB, name string) {
|
||||||
|
_m.Called(db, name)
|
||||||
|
}
|
||||||
|
|
||||||
type mockConstructorTestingTNewMetricsInterface interface {
|
type mockConstructorTestingTNewMetricsInterface interface {
|
||||||
mock.TestingT
|
mock.TestingT
|
||||||
Cleanup(func())
|
Cleanup(func())
|
||||||
|
|||||||
@@ -10123,6 +10123,24 @@ func (s *OpenTracingLayerThreadStore) DeleteMembershipForUser(userId string, pos
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteMembershipsForChannel")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (s *OpenTracingLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows")
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.DeleteOrphanedRows")
|
||||||
|
|||||||
@@ -11563,6 +11563,27 @@ func (s *RetryLayerThreadStore) DeleteMembershipForUser(userId string, postID st
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (s *RetryLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor
|
|||||||
Id: newCategoryId,
|
Id: newCategoryId,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
TeamId: teamId,
|
TeamId: teamId,
|
||||||
Sorting: model.SidebarCategorySortDefault,
|
Sorting: newCategory.Sorting,
|
||||||
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
|
SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list
|
||||||
Type: model.SidebarCategoryCustom,
|
Type: model.SidebarCategoryCustom,
|
||||||
Muted: newCategory.Muted,
|
Muted: newCategory.Muted,
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ package sqlstore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
@@ -66,14 +69,18 @@ type sqlxDBWrapper struct {
|
|||||||
*sqlx.DB
|
*sqlx.DB
|
||||||
queryTimeout time.Duration
|
queryTimeout time.Duration
|
||||||
trace bool
|
trace bool
|
||||||
|
isOnline *atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper {
|
func newSqlxDBWrapper(db *sqlx.DB, timeout time.Duration, trace bool) *sqlxDBWrapper {
|
||||||
return &sqlxDBWrapper{
|
w := &sqlxDBWrapper{
|
||||||
DB: db,
|
DB: db,
|
||||||
queryTimeout: timeout,
|
queryTimeout: timeout,
|
||||||
trace: trace,
|
trace: trace,
|
||||||
|
isOnline: &atomic.Bool{},
|
||||||
}
|
}
|
||||||
|
w.isOnline.Store(true)
|
||||||
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
||||||
@@ -83,19 +90,19 @@ func (w *sqlxDBWrapper) Stats() sql.DBStats {
|
|||||||
func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) {
|
func (w *sqlxDBWrapper) Beginx() (*sqlxTxWrapper, error) {
|
||||||
tx, err := w.DB.Beginx()
|
tx, err := w.DB.Beginx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, w.checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
|
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) {
|
func (w *sqlxDBWrapper) BeginXWithIsolation(opts *sql.TxOptions) (*sqlxTxWrapper, error) {
|
||||||
tx, err := w.DB.BeginTxx(context.Background(), opts)
|
tx, err := w.DB.BeginTxx(context.Background(), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, w.checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace), nil
|
return newSqlxTxWrapper(tx, w.queryTimeout, w.trace, w), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
||||||
@@ -109,7 +116,7 @@ func (w *sqlxDBWrapper) Get(dest any, query string, args ...any) error {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.DB.GetContext(ctx, dest, query, args...)
|
return w.checkErr(w.DB.GetContext(ctx, dest, query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error {
|
func (w *sqlxDBWrapper) GetBuilder(dest any, builder Builder) error {
|
||||||
@@ -134,7 +141,7 @@ func (w *sqlxDBWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.DB.NamedExecContext(ctx, query, arg)
|
return w.checkErrWithResult(w.DB.NamedExecContext(ctx, query, arg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
func (w *sqlxDBWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
||||||
@@ -161,7 +168,7 @@ func (w *sqlxDBWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.DB.ExecContext(context.Background(), query, args...)
|
return w.checkErrWithResult(w.DB.ExecContext(context.Background(), query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecRaw is like Exec but without any rebinding of params. You need to pass
|
// ExecRaw is like Exec but without any rebinding of params. You need to pass
|
||||||
@@ -176,7 +183,7 @@ func (w *sqlxDBWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.DB.ExecContext(ctx, query, args...)
|
return w.checkErrWithResult(w.DB.ExecContext(ctx, query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||||
@@ -192,7 +199,7 @@ func (w *sqlxDBWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.DB.NamedQueryContext(ctx, query, arg)
|
return w.checkErrWithRows(w.DB.NamedQueryContext(ctx, query, arg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
func (w *sqlxDBWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
||||||
@@ -220,7 +227,7 @@ func (w *sqlxDBWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.DB.QueryxContext(ctx, query, args)
|
return w.checkErrWithRows(w.DB.QueryxContext(ctx, query, args))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error {
|
func (w *sqlxDBWrapper) Select(dest any, query string, args ...any) error {
|
||||||
@@ -238,7 +245,7 @@ func (w *sqlxDBWrapper) SelectCtx(ctx context.Context, dest any, query string, a
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.DB.SelectContext(ctx, dest, query, args...)
|
return w.checkErr(w.DB.SelectContext(ctx, dest, query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error {
|
func (w *sqlxDBWrapper) SelectBuilder(dest any, builder Builder) error {
|
||||||
@@ -254,13 +261,15 @@ type sqlxTxWrapper struct {
|
|||||||
*sqlx.Tx
|
*sqlx.Tx
|
||||||
queryTimeout time.Duration
|
queryTimeout time.Duration
|
||||||
trace bool
|
trace bool
|
||||||
|
dbw *sqlxDBWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool) *sqlxTxWrapper {
|
func newSqlxTxWrapper(tx *sqlx.Tx, timeout time.Duration, trace bool, dbw *sqlxDBWrapper) *sqlxTxWrapper {
|
||||||
return &sqlxTxWrapper{
|
return &sqlxTxWrapper{
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
queryTimeout: timeout,
|
queryTimeout: timeout,
|
||||||
trace: trace,
|
trace: trace,
|
||||||
|
dbw: dbw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +284,7 @@ func (w *sqlxTxWrapper) Get(dest any, query string, args ...any) error {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Tx.GetContext(ctx, dest, query, args...)
|
return w.dbw.checkErr(w.Tx.GetContext(ctx, dest, query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
||||||
@@ -284,13 +293,13 @@ func (w *sqlxTxWrapper) GetBuilder(dest any, builder Builder) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Get(dest, query, args...)
|
return w.dbw.checkErr(w.Get(dest, query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
func (w *sqlxTxWrapper) Exec(query string, args ...any) (sql.Result, error) {
|
||||||
query = w.Tx.Rebind(query)
|
query = w.Tx.Rebind(query)
|
||||||
|
|
||||||
return w.ExecRaw(query, args...)
|
return w.dbw.checkErrWithResult(w.ExecRaw(query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
|
func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, error) {
|
||||||
@@ -302,7 +311,7 @@ func (w *sqlxTxWrapper) ExecNoTimeout(query string, args ...any) (sql.Result, er
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Tx.ExecContext(context.Background(), query, args...)
|
return w.dbw.checkErrWithResult(w.Tx.ExecContext(context.Background(), query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
|
func (w *sqlxTxWrapper) ExecBuilder(builder Builder) (sql.Result, error) {
|
||||||
@@ -326,7 +335,7 @@ func (w *sqlxTxWrapper) ExecRaw(query string, args ...any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Tx.ExecContext(ctx, query, args...)
|
return w.dbw.checkErrWithResult(w.Tx.ExecContext(ctx, query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
||||||
@@ -342,7 +351,7 @@ func (w *sqlxTxWrapper) NamedExec(query string, arg any) (sql.Result, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Tx.NamedExecContext(ctx, query, arg)
|
return w.dbw.checkErrWithResult(w.Tx.NamedExecContext(ctx, query, arg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
||||||
@@ -386,7 +395,7 @@ func (w *sqlxTxWrapper) NamedQuery(query string, arg any) (*sqlx.Rows, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.rows, res.err
|
return res.rows, w.dbw.checkErr(res.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
func (w *sqlxTxWrapper) QueryRowX(query string, args ...any) *sqlx.Row {
|
||||||
@@ -414,7 +423,7 @@ func (w *sqlxTxWrapper) QueryX(query string, args ...any) (*sqlx.Rows, error) {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Tx.QueryxContext(ctx, query, args)
|
return w.dbw.checkErrWithRows(w.Tx.QueryxContext(ctx, query, args))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
||||||
@@ -428,7 +437,7 @@ func (w *sqlxTxWrapper) Select(dest any, query string, args ...any) error {
|
|||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Tx.SelectContext(ctx, dest, query, args...)
|
return w.dbw.checkErr(w.Tx.SelectContext(ctx, dest, query, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error {
|
func (w *sqlxTxWrapper) SelectBuilder(dest any, builder Builder) error {
|
||||||
@@ -459,3 +468,23 @@ func printArgs(query string, dur time.Duration, args ...any) {
|
|||||||
}
|
}
|
||||||
mlog.Debug(query, fields...)
|
mlog.Debug(query, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *sqlxDBWrapper) checkErrWithResult(res sql.Result, err error) (sql.Result, error) {
|
||||||
|
return res, w.checkErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *sqlxDBWrapper) checkErrWithRows(res *sqlx.Rows, err error) (*sqlx.Rows, error) {
|
||||||
|
return res, w.checkErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *sqlxDBWrapper) checkErr(err error) error {
|
||||||
|
var netError *net.OpError
|
||||||
|
if errors.As(err, &netError) && (!netError.Temporary() && !netError.Timeout()) {
|
||||||
|
w.isOnline.Store(false)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *sqlxDBWrapper) Online() bool {
|
||||||
|
return w.isOnline.Load()
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package sqlstore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -28,12 +29,14 @@ func TestSqlX(t *testing.T) {
|
|||||||
}
|
}
|
||||||
*settings.QueryTimeout = 1
|
*settings.QueryTimeout = 1
|
||||||
store := &SqlStore{
|
store := &SqlStore{
|
||||||
rrCounter: 0,
|
rrCounter: 0,
|
||||||
srCounter: 0,
|
srCounter: 0,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
|
quitMonitor: make(chan struct{}),
|
||||||
|
wgMonitor: &sync.WaitGroup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
store.initConnection()
|
require.NoError(t, store.initConnection())
|
||||||
|
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const (
|
|||||||
MySQLForeignKeyViolationErrorCode = 1452
|
MySQLForeignKeyViolationErrorCode = 1452
|
||||||
PGDuplicateObjectErrorCode = "42710"
|
PGDuplicateObjectErrorCode = "42710"
|
||||||
MySQLDuplicateObjectErrorCode = 1022
|
MySQLDuplicateObjectErrorCode = 1022
|
||||||
DBPingAttempts = 18
|
DBPingAttempts = 5
|
||||||
DBPingTimeoutSecs = 10
|
DBPingTimeoutSecs = 10
|
||||||
// This is a numerical version string by postgres. The format is
|
// This is a numerical version string by postgres. The format is
|
||||||
// 2 characters for major, minor, and patch version prior to 10.
|
// 2 characters for major, minor, and patch version prior to 10.
|
||||||
@@ -123,9 +123,9 @@ type SqlStore struct {
|
|||||||
|
|
||||||
masterX *sqlxDBWrapper
|
masterX *sqlxDBWrapper
|
||||||
|
|
||||||
ReplicaXs []*sqlxDBWrapper
|
ReplicaXs []*atomic.Pointer[sqlxDBWrapper]
|
||||||
|
|
||||||
searchReplicaXs []*sqlxDBWrapper
|
searchReplicaXs []*atomic.Pointer[sqlxDBWrapper]
|
||||||
|
|
||||||
replicaLagHandles []*dbsql.DB
|
replicaLagHandles []*dbsql.DB
|
||||||
stores SqlStoreStores
|
stores SqlStoreStores
|
||||||
@@ -138,17 +138,28 @@ type SqlStore struct {
|
|||||||
|
|
||||||
isBinaryParam bool
|
isBinaryParam bool
|
||||||
pgDefaultTextSearchConfig string
|
pgDefaultTextSearchConfig string
|
||||||
|
|
||||||
|
quitMonitor chan struct{}
|
||||||
|
wgMonitor *sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore {
|
func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlStore {
|
||||||
store := &SqlStore{
|
store := &SqlStore{
|
||||||
rrCounter: 0,
|
rrCounter: 0,
|
||||||
srCounter: 0,
|
srCounter: 0,
|
||||||
settings: &settings,
|
settings: &settings,
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
|
quitMonitor: make(chan struct{}),
|
||||||
|
wgMonitor: &sync.WaitGroup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
store.initConnection()
|
err := store.initConnection()
|
||||||
|
if err != nil {
|
||||||
|
mlog.Fatal("Error setting up connections", mlog.Err(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
store.wgMonitor.Add(1)
|
||||||
|
go store.monitorReplicas()
|
||||||
|
|
||||||
ver, err := store.GetDbVersion(true)
|
ver, err := store.GetDbVersion(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -230,29 +241,28 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
|
|||||||
|
|
||||||
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
|
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
|
||||||
// It also applies any database configuration settings that are required.
|
// It also applies any database configuration settings that are required.
|
||||||
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings) *dbsql.DB {
|
func SetupConnection(connType string, dataSource string, settings *model.SqlSettings, attempts int) (*dbsql.DB, error) {
|
||||||
db, err := dbsql.Open(*settings.DriverName, dataSource)
|
db, err := dbsql.Open(*settings.DriverName, dataSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mlog.Fatal("Failed to open SQL connection to err.", mlog.Err(err))
|
return nil, errors.Wrap(err, "failed to open SQL connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < DBPingAttempts; i++ {
|
for i := 0; i < attempts; i++ {
|
||||||
// At this point, we have passed sql.Open, so we deliberately ignore any errors.
|
// At this point, we have passed sql.Open, so we deliberately ignore any errors.
|
||||||
sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource)
|
sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource)
|
||||||
mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", sanitized))
|
mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", sanitized))
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err = db.PingContext(ctx)
|
err = db.PingContext(ctx)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
break
|
if i == attempts-1 {
|
||||||
} else {
|
return nil, err
|
||||||
if i == DBPingAttempts-1 {
|
|
||||||
mlog.Fatal("Failed to ping DB, server will exit.", mlog.Err(err))
|
|
||||||
} else {
|
|
||||||
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
|
|
||||||
time.Sleep(DBPingTimeoutSecs * time.Second)
|
|
||||||
}
|
}
|
||||||
|
mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DBPingTimeoutSecs))
|
||||||
|
time.Sleep(DBPingTimeoutSecs * time.Second)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(connType, replicaLagPrefix) {
|
if strings.HasPrefix(connType, replicaLagPrefix) {
|
||||||
@@ -272,7 +282,7 @@ func SetupConnection(connType string, dataSource string, settings *model.SqlSett
|
|||||||
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
|
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
|
||||||
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
|
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
|
||||||
|
|
||||||
return db
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) SetContext(context context.Context) {
|
func (ss *SqlStore) SetContext(context context.Context) {
|
||||||
@@ -285,7 +295,7 @@ func (ss *SqlStore) Context() context.Context {
|
|||||||
|
|
||||||
func noOpMapper(s string) string { return s }
|
func noOpMapper(s string) string { return s }
|
||||||
|
|
||||||
func (ss *SqlStore) initConnection() {
|
func (ss *SqlStore) initConnection() error {
|
||||||
dataSource := *ss.settings.DataSource
|
dataSource := *ss.settings.DataSource
|
||||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||||
// TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout
|
// TODO: We ignore the readTimeout datasource parameter for MySQL since QueryTimeout
|
||||||
@@ -294,11 +304,14 @@ func (ss *SqlStore) initConnection() {
|
|||||||
var err error
|
var err error
|
||||||
dataSource, err = ResetReadTimeout(dataSource)
|
dataSource, err = ResetReadTimeout(dataSource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mlog.Fatal("Failed to reset read timeout from datasource.", mlog.Err(err), mlog.String("src", dataSource))
|
return errors.Wrap(err, "failed to reset read timeout from datasource")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle := SetupConnection("master", dataSource, ss.settings)
|
handle, err := SetupConnection("master", dataSource, ss.settings, DBPingAttempts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
ss.masterX = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||||
*ss.settings.Trace)
|
*ss.settings.Trace)
|
||||||
@@ -310,34 +323,32 @@ func (ss *SqlStore) initConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(ss.settings.DataSourceReplicas) > 0 {
|
if len(ss.settings.DataSourceReplicas) > 0 {
|
||||||
ss.ReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceReplicas))
|
ss.ReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceReplicas))
|
||||||
for i, replica := range ss.settings.DataSourceReplicas {
|
for i, replica := range ss.settings.DataSourceReplicas {
|
||||||
handle := SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings)
|
ss.ReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{}
|
||||||
ss.ReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
handle, err = SetupConnection(fmt.Sprintf("replica-%v", i), replica, ss.settings, DBPingAttempts)
|
||||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
if err != nil {
|
||||||
*ss.settings.Trace)
|
// Initializing to be offline
|
||||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
ss.ReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}})
|
||||||
ss.ReplicaXs[i].MapperFunc(noOpMapper)
|
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("replica-%v", i)), mlog.Err(err))
|
||||||
}
|
continue
|
||||||
if ss.metrics != nil {
|
|
||||||
ss.metrics.RegisterDBCollector(ss.ReplicaXs[i].DB.DB, "replica-"+strconv.Itoa(i))
|
|
||||||
}
|
}
|
||||||
|
ss.setDB(ss.ReplicaXs[i], handle, "replica-"+strconv.Itoa(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ss.settings.DataSourceSearchReplicas) > 0 {
|
if len(ss.settings.DataSourceSearchReplicas) > 0 {
|
||||||
ss.searchReplicaXs = make([]*sqlxDBWrapper, len(ss.settings.DataSourceSearchReplicas))
|
ss.searchReplicaXs = make([]*atomic.Pointer[sqlxDBWrapper], len(ss.settings.DataSourceSearchReplicas))
|
||||||
for i, replica := range ss.settings.DataSourceSearchReplicas {
|
for i, replica := range ss.settings.DataSourceSearchReplicas {
|
||||||
handle := SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings)
|
ss.searchReplicaXs[i] = &atomic.Pointer[sqlxDBWrapper]{}
|
||||||
ss.searchReplicaXs[i] = newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
handle, err = SetupConnection(fmt.Sprintf("search-replica-%v", i), replica, ss.settings, DBPingAttempts)
|
||||||
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
if err != nil {
|
||||||
*ss.settings.Trace)
|
// Initializing to be offline
|
||||||
if ss.DriverName() == model.DatabaseDriverMysql {
|
ss.searchReplicaXs[i].Store(&sqlxDBWrapper{isOnline: &atomic.Bool{}})
|
||||||
ss.searchReplicaXs[i].MapperFunc(noOpMapper)
|
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", fmt.Sprintf("search-replica-%v", i)), mlog.Err(err))
|
||||||
}
|
continue
|
||||||
if ss.metrics != nil {
|
|
||||||
ss.metrics.RegisterDBCollector(ss.searchReplicaXs[i].DB.DB, "searchreplica-"+strconv.Itoa(i))
|
|
||||||
}
|
}
|
||||||
|
ss.setDB(ss.searchReplicaXs[i], handle, "searchreplica-"+strconv.Itoa(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,9 +358,14 @@ func (ss *SqlStore) initConnection() {
|
|||||||
if src.DataSource == nil {
|
if src.DataSource == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ss.replicaLagHandles[i] = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings)
|
ss.replicaLagHandles[i], err = SetupConnection(fmt.Sprintf(replicaLagPrefix+"-%d", i), *src.DataSource, ss.settings, DBPingAttempts)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Warn("Failed to setup replica lag handle. Skipping..", mlog.String("db", fmt.Sprintf(replicaLagPrefix+"-%d", i)), mlog.Err(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) DriverName() string {
|
func (ss *SqlStore) DriverName() string {
|
||||||
@@ -455,8 +471,15 @@ func (ss *SqlStore) GetSearchReplicaX() *sqlxDBWrapper {
|
|||||||
return ss.GetReplicaX()
|
return ss.GetReplicaX()
|
||||||
}
|
}
|
||||||
|
|
||||||
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
|
for i := 0; i < len(ss.searchReplicaXs); i++ {
|
||||||
return ss.searchReplicaXs[rrNum]
|
rrNum := atomic.AddInt64(&ss.srCounter, 1) % int64(len(ss.searchReplicaXs))
|
||||||
|
if ss.searchReplicaXs[rrNum].Load().Online() {
|
||||||
|
return ss.searchReplicaXs[rrNum].Load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all search replicas are down, then go with replica.
|
||||||
|
return ss.GetReplicaX()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
||||||
@@ -464,23 +487,64 @@ func (ss *SqlStore) GetReplicaX() *sqlxDBWrapper {
|
|||||||
return ss.GetMasterX()
|
return ss.GetMasterX()
|
||||||
}
|
}
|
||||||
|
|
||||||
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
for i := 0; i < len(ss.ReplicaXs); i++ {
|
||||||
return ss.ReplicaXs[rrNum]
|
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
||||||
}
|
if ss.ReplicaXs[rrNum].Load().Online() {
|
||||||
|
return ss.ReplicaXs[rrNum].Load()
|
||||||
func (ss *SqlStore) GetInternalReplicaDBs() []*sql.DB {
|
|
||||||
if len(ss.settings.DataSourceReplicas) == 0 || ss.lockedToMaster || !ss.hasLicense() {
|
|
||||||
return []*sql.DB{
|
|
||||||
ss.GetMasterX().DB.DB,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbs := make([]*sql.DB, len(ss.ReplicaXs))
|
// If all replicas are down, then go with master.
|
||||||
for i, rx := range ss.ReplicaXs {
|
return ss.GetMasterX()
|
||||||
dbs[i] = rx.DB.DB
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return dbs
|
func (ss *SqlStore) monitorReplicas() {
|
||||||
|
t := time.NewTicker(time.Duration(*ss.settings.ReplicaMonitorIntervalSeconds) * time.Second)
|
||||||
|
defer func() {
|
||||||
|
t.Stop()
|
||||||
|
ss.wgMonitor.Done()
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ss.quitMonitor:
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
setupReplica := func(r *atomic.Pointer[sqlxDBWrapper], dsn, name string) {
|
||||||
|
if r.Load().Online() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, err := SetupConnection(name, dsn, ss.settings, 1)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Warn("Failed to setup connection. Skipping..", mlog.String("db", name), mlog.Err(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ss.metrics != nil && r.Load() != nil && r.Load().DB != nil {
|
||||||
|
ss.metrics.UnregisterDBCollector(r.Load().DB.DB, name)
|
||||||
|
}
|
||||||
|
ss.setDB(r, handle, name)
|
||||||
|
}
|
||||||
|
for i, replica := range ss.ReplicaXs {
|
||||||
|
setupReplica(replica, ss.settings.DataSourceReplicas[i], "replica-"+strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, replica := range ss.searchReplicaXs {
|
||||||
|
setupReplica(replica, ss.settings.DataSourceSearchReplicas[i], "search-replica-"+strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SqlStore) setDB(replica *atomic.Pointer[sqlxDBWrapper], handle *dbsql.DB, name string) {
|
||||||
|
replica.Store(newSqlxDBWrapper(sqlx.NewDb(handle, ss.DriverName()),
|
||||||
|
time.Duration(*ss.settings.QueryTimeout)*time.Second,
|
||||||
|
*ss.settings.Trace))
|
||||||
|
if ss.DriverName() == model.DatabaseDriverMysql {
|
||||||
|
replica.Load().MapperFunc(noOpMapper)
|
||||||
|
}
|
||||||
|
if ss.metrics != nil {
|
||||||
|
ss.metrics.RegisterDBCollector(replica.Load().DB.DB, name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
||||||
@@ -489,7 +553,7 @@ func (ss *SqlStore) GetInternalReplicaDB() *sql.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
rrNum := atomic.AddInt64(&ss.rrCounter, 1) % int64(len(ss.ReplicaXs))
|
||||||
return ss.ReplicaXs[rrNum].DB.DB
|
return ss.ReplicaXs[rrNum].Load().DB.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) TotalMasterDbConnections() int {
|
func (ss *SqlStore) TotalMasterDbConnections() int {
|
||||||
@@ -541,7 +605,10 @@ func (ss *SqlStore) TotalReadDbConnections() int {
|
|||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
for _, db := range ss.ReplicaXs {
|
for _, db := range ss.ReplicaXs {
|
||||||
count = count + db.Stats().OpenConnections
|
if !db.Load().Online() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count = count + db.Load().Stats().OpenConnections
|
||||||
}
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
@@ -554,7 +621,10 @@ func (ss *SqlStore) TotalSearchDbConnections() int {
|
|||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
for _, db := range ss.searchReplicaXs {
|
for _, db := range ss.searchReplicaXs {
|
||||||
count = count + db.Stats().OpenConnections
|
if !db.Load().Online() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count = count + db.Load().Stats().OpenConnections
|
||||||
}
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
@@ -782,9 +852,14 @@ func IsUniqueConstraintError(err error, indexName []string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper {
|
func (ss *SqlStore) GetAllConns() []*sqlxDBWrapper {
|
||||||
all := make([]*sqlxDBWrapper, len(ss.ReplicaXs)+1)
|
all := make([]*sqlxDBWrapper, 0, len(ss.ReplicaXs)+1)
|
||||||
copy(all, ss.ReplicaXs)
|
for i := range ss.ReplicaXs {
|
||||||
all[len(ss.ReplicaXs)] = ss.masterX
|
if !ss.ReplicaXs[i].Load().Online() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
all = append(all, ss.ReplicaXs[i].Load())
|
||||||
|
}
|
||||||
|
all = append(all, ss.masterX)
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,11 +882,24 @@ func (ss *SqlStore) RecycleDBConnections(d time.Duration) {
|
|||||||
|
|
||||||
func (ss *SqlStore) Close() {
|
func (ss *SqlStore) Close() {
|
||||||
ss.masterX.Close()
|
ss.masterX.Close()
|
||||||
|
// Closing monitor and waiting for it to be done.
|
||||||
|
// This needs to be done before closing the replica handles.
|
||||||
|
close(ss.quitMonitor)
|
||||||
|
ss.wgMonitor.Wait()
|
||||||
|
|
||||||
for _, replica := range ss.ReplicaXs {
|
for _, replica := range ss.ReplicaXs {
|
||||||
replica.Close()
|
if replica.Load().Online() {
|
||||||
|
replica.Load().Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, replica := range ss.searchReplicaXs {
|
for _, replica := range ss.searchReplicaXs {
|
||||||
|
if replica.Load().Online() {
|
||||||
|
replica.Load().Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, replica := range ss.replicaLagHandles {
|
||||||
replica.Close()
|
replica.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1132,7 +1220,10 @@ func (ss *SqlStore) migrate(direction migrationDirection) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
db := SetupConnection("master", dataSource, ss.settings)
|
db, err2 := SetupConnection("master", dataSource, ss.settings, DBPingAttempts)
|
||||||
|
if err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
driver, err = ms.WithInstance(db)
|
driver, err = ms.WithInstance(db)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
case model.DatabaseDriverPostgres:
|
case model.DatabaseDriverPostgres:
|
||||||
|
|||||||
@@ -761,13 +761,15 @@ func TestReplicaLagQuery(t *testing.T) {
|
|||||||
mockMetrics.On("RegisterDBCollector", mock.AnythingOfType("*sql.DB"), "master")
|
mockMetrics.On("RegisterDBCollector", mock.AnythingOfType("*sql.DB"), "master")
|
||||||
|
|
||||||
store := &SqlStore{
|
store := &SqlStore{
|
||||||
rrCounter: 0,
|
rrCounter: 0,
|
||||||
srCounter: 0,
|
srCounter: 0,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
metrics: mockMetrics,
|
metrics: mockMetrics,
|
||||||
|
quitMonitor: make(chan struct{}),
|
||||||
|
wgMonitor: &sync.WaitGroup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
store.initConnection()
|
require.NoError(t, store.initConnection())
|
||||||
store.stores.post = newSqlPostStore(store, mockMetrics)
|
store.stores.post = newSqlPostStore(store, mockMetrics)
|
||||||
err = store.migrate(migrationsDirectionUp)
|
err = store.migrate(migrationsDirectionUp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -839,9 +841,11 @@ func TestMySQLReadTimeout(t *testing.T) {
|
|||||||
settings.DataSource = &dataSource
|
settings.DataSource = &dataSource
|
||||||
|
|
||||||
store := &SqlStore{
|
store := &SqlStore{
|
||||||
settings: settings,
|
settings: settings,
|
||||||
|
quitMonitor: make(chan struct{}),
|
||||||
|
wgMonitor: &sync.WaitGroup{},
|
||||||
}
|
}
|
||||||
store.initConnection()
|
require.NoError(t, store.initConnection())
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
_, err = store.GetMasterX().ExecNoTimeout(`SELECT SLEEP(3)`)
|
_, err = store.GetMasterX().ExecNoTimeout(`SELECT SLEEP(3)`)
|
||||||
|
|||||||
@@ -688,6 +688,28 @@ func (s *SqlThreadStore) UpdateMembership(membership *model.ThreadMembership) (*
|
|||||||
return s.updateMembership(s.GetMasterX(), membership)
|
return s.updateMembership(s.GetMasterX(), membership)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SqlThreadStore) DeleteMembershipsForChannel(userID, channelID string) error {
|
||||||
|
subQuery := s.getSubQueryBuilder().
|
||||||
|
Select("1").
|
||||||
|
From("Threads").
|
||||||
|
Where(sq.And{
|
||||||
|
sq.Expr("Threads.PostId = ThreadMemberships.PostId"),
|
||||||
|
sq.Eq{"Threads.ChannelId": channelID},
|
||||||
|
})
|
||||||
|
|
||||||
|
query := s.getQueryBuilder().
|
||||||
|
Delete("ThreadMemberships").
|
||||||
|
Where(sq.Eq{"UserId": userID}).
|
||||||
|
Where(sq.Expr("EXISTS (?)", subQuery))
|
||||||
|
|
||||||
|
_, err := s.GetMasterX().ExecBuilder(query)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to remove thread memberships with userid=%s channelid=%s", userID, channelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
|
func (s *SqlThreadStore) updateMembership(ex sqlxExecutor, membership *model.ThreadMembership) (*model.ThreadMembership, error) {
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Update("ThreadMemberships").
|
Update("ThreadMemberships").
|
||||||
@@ -712,7 +734,14 @@ func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model.
|
|||||||
memberships := []*model.ThreadMembership{}
|
memberships := []*model.ThreadMembership{}
|
||||||
|
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select("ThreadMemberships.*").
|
Select(
|
||||||
|
"ThreadMemberships.PostId",
|
||||||
|
"ThreadMemberships.UserId",
|
||||||
|
"ThreadMemberships.Following",
|
||||||
|
"ThreadMemberships.LastUpdated",
|
||||||
|
"ThreadMemberships.LastViewed",
|
||||||
|
"ThreadMemberships.UnreadMentions",
|
||||||
|
).
|
||||||
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
|
Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
|
||||||
From("ThreadMemberships").
|
From("ThreadMemberships").
|
||||||
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
|
Where(sq.Or{sq.Eq{"Threads.ThreadTeamId": teamId}, sq.Eq{"Threads.ThreadTeamId": ""}}).
|
||||||
@@ -732,7 +761,14 @@ func (s *SqlThreadStore) GetMembershipForUser(userId, postId string) (*model.Thr
|
|||||||
func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) {
|
func (s *SqlThreadStore) getMembershipForUser(ex sqlxExecutor, userId, postId string) (*model.ThreadMembership, error) {
|
||||||
var membership model.ThreadMembership
|
var membership model.ThreadMembership
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select("*").
|
Select(
|
||||||
|
"PostId",
|
||||||
|
"UserId",
|
||||||
|
"Following",
|
||||||
|
"LastUpdated",
|
||||||
|
"LastViewed",
|
||||||
|
"UnreadMentions",
|
||||||
|
).
|
||||||
From("ThreadMemberships").
|
From("ThreadMemberships").
|
||||||
Where(sq.And{
|
Where(sq.And{
|
||||||
sq.Eq{"PostId": postId},
|
sq.Eq{"PostId": postId},
|
||||||
|
|||||||
@@ -72,10 +72,7 @@ type Store interface {
|
|||||||
// GetInternalMasterDB allows access to the raw master DB
|
// GetInternalMasterDB allows access to the raw master DB
|
||||||
// handle for the multi-product architecture.
|
// handle for the multi-product architecture.
|
||||||
GetInternalMasterDB() *sql.DB
|
GetInternalMasterDB() *sql.DB
|
||||||
// GetInternalReplicaDBs allows access to the raw replica DB
|
|
||||||
// handles for the multi-product architecture.
|
|
||||||
GetInternalReplicaDB() *sql.DB
|
GetInternalReplicaDB() *sql.DB
|
||||||
GetInternalReplicaDBs() []*sql.DB
|
|
||||||
TotalMasterDbConnections() int
|
TotalMasterDbConnections() int
|
||||||
TotalReadDbConnections() int
|
TotalReadDbConnections() int
|
||||||
TotalSearchDbConnections() int
|
TotalSearchDbConnections() int
|
||||||
@@ -347,6 +344,7 @@ type ThreadStore interface {
|
|||||||
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
|
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
|
||||||
DeleteOrphanedRows(limit int) (deleted int64, err error)
|
DeleteOrphanedRows(limit int) (deleted int64, err error)
|
||||||
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
|
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
|
||||||
|
DeleteMembershipsForChannel(userID, channelID string) error
|
||||||
|
|
||||||
// Insights - threads
|
// Insights - threads
|
||||||
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
|
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
|
||||||
|
|||||||
@@ -672,6 +672,38 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{}, res2.Channels)
|
assert.Equal(t, []string{}, res2.Channels)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should store the correct sorting value", func(t *testing.T) {
|
||||||
|
userId := model.NewId()
|
||||||
|
|
||||||
|
team := setupTeam(t, ss, userId)
|
||||||
|
|
||||||
|
opts := &store.SidebarCategorySearchOpts{
|
||||||
|
TeamID: team.Id,
|
||||||
|
ExcludeTeam: false,
|
||||||
|
}
|
||||||
|
res, nErr := ss.Channel().CreateInitialSidebarCategories(userId, opts)
|
||||||
|
require.NoError(t, nErr)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
// Create the category
|
||||||
|
created, err := ss.Channel().CreateSidebarCategory(userId, team.Id, &model.SidebarCategoryWithChannels{
|
||||||
|
SidebarCategory: model.SidebarCategory{
|
||||||
|
DisplayName: model.NewId(),
|
||||||
|
Sorting: model.SidebarCategorySortManual,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Confirm that sorting value is correct
|
||||||
|
res, err = ss.Channel().GetSidebarCategoriesForTeamForUser(userId, team.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, res.Categories, 4)
|
||||||
|
// first category will be favorites and second will be newly created
|
||||||
|
assert.Equal(t, model.SidebarCategoryCustom, res.Categories[1].Type)
|
||||||
|
assert.Equal(t, created.Id, res.Categories[1].Id)
|
||||||
|
assert.Equal(t, model.SidebarCategorySortManual, res.Categories[1].Sorting)
|
||||||
|
assert.Equal(t, model.SidebarCategorySortManual, created.Sorting)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
|
func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlStore) {
|
||||||
|
|||||||
@@ -346,22 +346,6 @@ func (_m *Store) GetInternalReplicaDB() *sql.DB {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInternalReplicaDBs provides a mock function with given fields:
|
|
||||||
func (_m *Store) GetInternalReplicaDBs() []*sql.DB {
|
|
||||||
ret := _m.Called()
|
|
||||||
|
|
||||||
var r0 []*sql.DB
|
|
||||||
if rf, ok := ret.Get(0).(func() []*sql.DB); ok {
|
|
||||||
r0 = rf()
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*sql.DB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group provides a mock function with given fields:
|
// Group provides a mock function with given fields:
|
||||||
func (_m *Store) Group() store.GroupStore {
|
func (_m *Store) Group() store.GroupStore {
|
||||||
ret := _m.Called()
|
ret := _m.Called()
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ func (_m *ThreadStore) DeleteMembershipForUser(userId string, postID string) err
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteMembershipsForChannel provides a mock function with given fields: userID, channelID
|
||||||
|
func (_m *ThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||||
|
ret := _m.Called(userID, channelID)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||||
|
r0 = rf(userID, channelID)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteOrphanedRows provides a mock function with given fields: limit
|
// DeleteOrphanedRows provides a mock function with given fields: limit
|
||||||
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (_m *ThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
ret := _m.Called(limit)
|
ret := _m.Called(limit)
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ func MakeSqlSettings(driver string, withReplica bool) *model.SqlSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log("Created temporary " + driver + " database " + dbName)
|
log("Created temporary " + driver + " database " + dbName)
|
||||||
|
settings.ReplicaMonitorIntervalSeconds = model.NewInt(5)
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
|
|||||||
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
|
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
|
||||||
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
|
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
|
||||||
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
|
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
|
||||||
|
t.Run("DeleteMembershipsForChannel", func(t *testing.T) { testDeleteMembershipsForChannel(t, ss) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func testThreadStorePopulation(t *testing.T, ss store.Store) {
|
func testThreadStorePopulation(t *testing.T, ss store.Store) {
|
||||||
@@ -1914,3 +1915,121 @@ func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) {
|
|||||||
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
assertThreadReplyCount(t, userBID, team2.Id, 1, "expected 1 unread message in team2 for userB")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDeleteMembershipsForChannel(t *testing.T, ss store.Store) {
|
||||||
|
createThreadMembership := func(userID, postID string) (*model.ThreadMembership, func()) {
|
||||||
|
t.Helper()
|
||||||
|
opts := store.ThreadMembershipOpts{
|
||||||
|
Following: true,
|
||||||
|
IncrementMentions: false,
|
||||||
|
UpdateFollowing: true,
|
||||||
|
UpdateViewedTimestamp: false,
|
||||||
|
UpdateParticipants: false,
|
||||||
|
}
|
||||||
|
mem, err := ss.Thread().MaintainMembership(userID, postID, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return mem, func() {
|
||||||
|
err := ss.Thread().DeleteMembershipForUser(userID, postID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postingUserID := model.NewId()
|
||||||
|
userAID := model.NewId()
|
||||||
|
userBID := model.NewId()
|
||||||
|
|
||||||
|
team, err := ss.Team().Save(&model.Team{
|
||||||
|
DisplayName: "DisplayName",
|
||||||
|
Name: "team" + model.NewId(),
|
||||||
|
Email: MakeEmail(),
|
||||||
|
Type: model.TeamOpen,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
channel1, err := ss.Channel().Save(&model.Channel{
|
||||||
|
TeamId: team.Id,
|
||||||
|
DisplayName: "DisplayName",
|
||||||
|
Name: "channel1" + model.NewId(),
|
||||||
|
Type: model.ChannelTypeOpen,
|
||||||
|
}, -1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
channel2, err := ss.Channel().Save(&model.Channel{
|
||||||
|
TeamId: team.Id,
|
||||||
|
DisplayName: "DisplayName2",
|
||||||
|
Name: "channel2" + model.NewId(),
|
||||||
|
Type: model.ChannelTypeOpen,
|
||||||
|
}, -1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rootPost1, err := ss.Post().Save(&model.Post{
|
||||||
|
ChannelId: channel1.Id,
|
||||||
|
UserId: postingUserID,
|
||||||
|
Message: model.NewRandomString(10),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = ss.Post().Save(&model.Post{
|
||||||
|
ChannelId: channel1.Id,
|
||||||
|
UserId: postingUserID,
|
||||||
|
Message: model.NewRandomString(10),
|
||||||
|
RootId: rootPost1.Id,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rootPost2, err := ss.Post().Save(&model.Post{
|
||||||
|
ChannelId: channel2.Id,
|
||||||
|
UserId: postingUserID,
|
||||||
|
Message: model.NewRandomString(10),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = ss.Post().Save(&model.Post{
|
||||||
|
ChannelId: channel2.Id,
|
||||||
|
UserId: postingUserID,
|
||||||
|
Message: model.NewRandomString(10),
|
||||||
|
RootId: rootPost2.Id,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("should return memberships for user", func(t *testing.T) {
|
||||||
|
memA1, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
||||||
|
defer cleanupA1()
|
||||||
|
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
||||||
|
defer cleanupA2()
|
||||||
|
|
||||||
|
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, membershipsA, 2)
|
||||||
|
require.ElementsMatch(t, []*model.ThreadMembership{memA1, memA2}, membershipsA)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should delete memberships for user for channel", func(t *testing.T) {
|
||||||
|
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
||||||
|
defer cleanupA1()
|
||||||
|
memA2, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
||||||
|
defer cleanupA2()
|
||||||
|
|
||||||
|
ss.Thread().DeleteMembershipsForChannel(userAID, channel1.Id)
|
||||||
|
membershipsA, err := ss.Thread().GetMembershipsForUser(userAID, team.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, membershipsA, 1)
|
||||||
|
require.ElementsMatch(t, []*model.ThreadMembership{memA2}, membershipsA)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deleting memberships for channel for userA should not affect userB", func(t *testing.T) {
|
||||||
|
_, cleanupA1 := createThreadMembership(userAID, rootPost1.Id)
|
||||||
|
defer cleanupA1()
|
||||||
|
_, cleanupA2 := createThreadMembership(userAID, rootPost2.Id)
|
||||||
|
defer cleanupA2()
|
||||||
|
memB1, cleanupB2 := createThreadMembership(userBID, rootPost1.Id)
|
||||||
|
defer cleanupB2()
|
||||||
|
|
||||||
|
membershipsB, err := ss.Thread().GetMembershipsForUser(userBID, team.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, membershipsB, 1)
|
||||||
|
require.ElementsMatch(t, []*model.ThreadMembership{memB1}, membershipsB)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9112,6 +9112,22 @@ func (s *TimerLayerThreadStore) DeleteMembershipForUser(userId string, postID st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerThreadStore) DeleteMembershipsForChannel(userID string, channelID string) error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
err := s.ThreadStore.DeleteMembershipsForChannel(userID, channelID)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
|
if s.Root.Metrics != nil {
|
||||||
|
success := "false"
|
||||||
|
if err == nil {
|
||||||
|
success = "true"
|
||||||
|
}
|
||||||
|
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.DeleteMembershipsForChannel", success, elapsed)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
func (s *TimerLayerThreadStore) DeleteOrphanedRows(limit int) (int64, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ func (h *MainHelper) SetReplicationLagForTesting(seconds int) error {
|
|||||||
|
|
||||||
func (h *MainHelper) execOnEachReplica(query string, args ...any) error {
|
func (h *MainHelper) execOnEachReplica(query string, args ...any) error {
|
||||||
for _, replica := range h.SQLStore.ReplicaXs {
|
for _, replica := range h.SQLStore.ReplicaXs {
|
||||||
_, err := replica.Exec(query, args...)
|
_, err := replica.Load().Exec(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
|||||||
|
|
||||||
props["CWSURL"] = *c.CloudSettings.CWSURL
|
props["CWSURL"] = *c.CloudSettings.CWSURL
|
||||||
|
|
||||||
|
props["DisableRefetchingOnBrowserFocus"] = strconv.FormatBool(*c.ExperimentalSettings.DisableRefetchingOnBrowserFocus)
|
||||||
|
|
||||||
// Set default values for all options that require a license.
|
// Set default values for all options that require a license.
|
||||||
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
||||||
props["LdapNicknameAttributeSet"] = "false"
|
props["LdapNicknameAttributeSet"] = "false"
|
||||||
|
|||||||
@@ -1777,6 +1777,10 @@
|
|||||||
"id": "api.error_get_first_admin_visit_marketplace_status",
|
"id": "api.error_get_first_admin_visit_marketplace_status",
|
||||||
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
|
"translation": "Error trying to retrieve the first admin visit marketplace status from the store."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.error_no_organization_name_provided_for_self_hosted_onboarding",
|
||||||
|
"translation": "Error no organization name provided for self hosted onboarding."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.error_set_first_admin_complete_setup",
|
"id": "api.error_set_first_admin_complete_setup",
|
||||||
"translation": "Error trying to save first admin complete setup in the store."
|
"translation": "Error trying to save first admin complete setup in the store."
|
||||||
|
|||||||
@@ -974,6 +974,7 @@ type ExperimentalSettings struct {
|
|||||||
EnableRemoteClusterService *bool `access:"experimental_features"`
|
EnableRemoteClusterService *bool `access:"experimental_features"`
|
||||||
EnableAppBar *bool `access:"experimental_features"`
|
EnableAppBar *bool `access:"experimental_features"`
|
||||||
PatchPluginsReactDOM *bool `access:"experimental_features"`
|
PatchPluginsReactDOM *bool `access:"experimental_features"`
|
||||||
|
DisableRefetchingOnBrowserFocus *bool `access:"experimental_features"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExperimentalSettings) SetDefaults() {
|
func (s *ExperimentalSettings) SetDefaults() {
|
||||||
@@ -1012,6 +1013,10 @@ func (s *ExperimentalSettings) SetDefaults() {
|
|||||||
if s.PatchPluginsReactDOM == nil {
|
if s.PatchPluginsReactDOM == nil {
|
||||||
s.PatchPluginsReactDOM = NewBool(false)
|
s.PatchPluginsReactDOM = NewBool(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.DisableRefetchingOnBrowserFocus == nil {
|
||||||
|
s.DisableRefetchingOnBrowserFocus = NewBool(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnalyticsSettings struct {
|
type AnalyticsSettings struct {
|
||||||
@@ -1163,6 +1168,7 @@ type SqlSettings struct {
|
|||||||
DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"`
|
DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||||
MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||||
ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
|
ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none
|
||||||
|
ReplicaMonitorIntervalSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
||||||
@@ -1227,6 +1233,10 @@ func (s *SqlSettings) SetDefaults(isUpdate bool) {
|
|||||||
if s.ReplicaLagSettings == nil {
|
if s.ReplicaLagSettings == nil {
|
||||||
s.ReplicaLagSettings = []*ReplicaLagSettings{}
|
s.ReplicaLagSettings = []*ReplicaLagSettings{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.ReplicaMonitorIntervalSeconds == nil {
|
||||||
|
s.ReplicaMonitorIntervalSeconds = NewInt(5)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogSettings struct {
|
type LogSettings struct {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
// CompleteOnboardingRequest describes parameters of the requested plugin.
|
// CompleteOnboardingRequest describes parameters of the requested plugin.
|
||||||
type CompleteOnboardingRequest struct {
|
type CompleteOnboardingRequest struct {
|
||||||
|
Organization string `json:"organization"` // Organization is the name of the organization
|
||||||
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
|
InstallPlugins []string `json:"install_plugins"` // InstallPlugins is a list of plugins to be installed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ type Permission struct {
|
|||||||
|
|
||||||
var PermissionInviteUser *Permission
|
var PermissionInviteUser *Permission
|
||||||
var PermissionAddUserToTeam *Permission
|
var PermissionAddUserToTeam *Permission
|
||||||
|
|
||||||
|
// Deprecated: PermissionCreatePost should be used to determine if a slash command can be executed.
|
||||||
|
// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274
|
||||||
var PermissionUseSlashCommands *Permission
|
var PermissionUseSlashCommands *Permission
|
||||||
var PermissionManageSlashCommands *Permission
|
var PermissionManageSlashCommands *Permission
|
||||||
var PermissionManageOthersSlashCommands *Permission
|
var PermissionManageOthersSlashCommands *Permission
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const (
|
|||||||
SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey"
|
SystemAsymmetricSigningKeyKey = "AsymmetricSigningKey"
|
||||||
SystemPostActionCookieSecretKey = "PostActionCookieSecret"
|
SystemPostActionCookieSecretKey = "PostActionCookieSecret"
|
||||||
SystemInstallationDateKey = "InstallationDate"
|
SystemInstallationDateKey = "InstallationDate"
|
||||||
|
SystemOrganizationName = "OrganizationName"
|
||||||
SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp"
|
SystemFirstServerRunTimestampKey = "FirstServerRunTimestamp"
|
||||||
SystemClusterEncryptionKey = "ClusterEncryptionKey"
|
SystemClusterEncryptionKey = "ClusterEncryptionKey"
|
||||||
SystemUpgradedFromTeId = "UpgradedFromTE"
|
SystemUpgradedFromTeId = "UpgradedFromTE"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
// It should be maintained in chronological order with most current
|
// It should be maintained in chronological order with most current
|
||||||
// release at the front of the list.
|
// release at the front of the list.
|
||||||
var versions = []string{
|
var versions = []string{
|
||||||
|
"7.11.0",
|
||||||
"7.10.0",
|
"7.10.0",
|
||||||
"7.9.0",
|
"7.9.0",
|
||||||
"7.8.0",
|
"7.8.0",
|
||||||
|
|||||||
@@ -521,6 +521,7 @@ func (ts *TelemetryService) trackConfig() {
|
|||||||
"query_timeout": *cfg.SqlSettings.QueryTimeout,
|
"query_timeout": *cfg.SqlSettings.QueryTimeout,
|
||||||
"disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch,
|
"disable_database_search": *cfg.SqlSettings.DisableDatabaseSearch,
|
||||||
"migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds,
|
"migrations_statement_timeout_seconds": *cfg.SqlSettings.MigrationsStatementTimeoutSeconds,
|
||||||
|
"replica_monitor_interval_seconds": *cfg.SqlSettings.ReplicaMonitorIntervalSeconds,
|
||||||
})
|
})
|
||||||
|
|
||||||
ts.SendTelemetry(TrackConfigLog, map[string]any{
|
ts.SendTelemetry(TrackConfigLog, map[string]any{
|
||||||
@@ -749,15 +750,16 @@ func (ts *TelemetryService) trackConfig() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ts.SendTelemetry(TrackConfigExperimental, map[string]any{
|
ts.SendTelemetry(TrackConfigExperimental, map[string]any{
|
||||||
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
|
"client_side_cert_enable": *cfg.ExperimentalSettings.ClientSideCertEnable,
|
||||||
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
|
"isdefault_client_side_cert_check": isDefault(*cfg.ExperimentalSettings.ClientSideCertCheck, model.ClientSideCertCheckPrimaryAuth),
|
||||||
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
|
"link_metadata_timeout_milliseconds": *cfg.ExperimentalSettings.LinkMetadataTimeoutMilliseconds,
|
||||||
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
|
"restrict_system_admin": *cfg.ExperimentalSettings.RestrictSystemAdmin,
|
||||||
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
|
"use_new_saml_library": *cfg.ExperimentalSettings.UseNewSAMLLibrary,
|
||||||
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
|
"enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels,
|
||||||
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
|
"enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService,
|
||||||
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
|
"enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar,
|
||||||
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
|
"patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM,
|
||||||
|
"disable_refetching_on_browser_focus": *cfg.ExperimentalSettings.DisableRefetchingOnBrowserFocus,
|
||||||
})
|
})
|
||||||
|
|
||||||
ts.SendTelemetry(TrackConfigAnalytics, map[string]any{
|
ts.SendTelemetry(TrackConfigAnalytics, map[string]any{
|
||||||
|
|||||||
@@ -254,25 +254,22 @@ export function fetchChannelsAndMembers(teamId: Team['id'] = ''): ActionFunc<{ch
|
|||||||
teamId,
|
teamId,
|
||||||
data: channels,
|
data: channels,
|
||||||
});
|
});
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
|
|
||||||
data: channelMembers,
|
|
||||||
});
|
|
||||||
actions.push({
|
|
||||||
type: RoleTypes.RECEIVED_ROLES,
|
|
||||||
data: roles,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
actions.push({
|
actions.push({
|
||||||
type: ChannelTypes.RECEIVED_ALL_CHANNELS,
|
type: ChannelTypes.RECEIVED_ALL_CHANNELS,
|
||||||
data: channels,
|
data: channels,
|
||||||
});
|
});
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
|
|
||||||
data: channelMembers,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBERS,
|
||||||
|
data: channelMembers,
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
type: RoleTypes.RECEIVED_ROLES,
|
||||||
|
data: roles,
|
||||||
|
});
|
||||||
|
|
||||||
await dispatch(batchActions(actions));
|
await dispatch(batchActions(actions));
|
||||||
|
|
||||||
return {data: {channels, channelMembers, roles}};
|
return {data: {channels, channelMembers, roles}};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {Preferences} from 'mattermost-redux/constants';
|
|||||||
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {getBool, isCollapsedThreadsEnabled, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getCurrentUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels';
|
import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||||
import {ChannelTypes} from 'mattermost-redux/action_types';
|
import {ChannelTypes} from 'mattermost-redux/action_types';
|
||||||
@@ -367,11 +367,19 @@ export async function redirectUserToDefaultTeam() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the user is the first admin
|
||||||
|
const isUserFirstAdmin = isFirstAdmin(state);
|
||||||
|
|
||||||
const locale = getCurrentLocale(state);
|
const locale = getCurrentLocale(state);
|
||||||
const teamId = LocalStorageStore.getPreviousTeamId(user.id);
|
const teamId = LocalStorageStore.getPreviousTeamId(user.id);
|
||||||
|
|
||||||
let myTeams = getMyTeams(state);
|
let myTeams = getMyTeams(state);
|
||||||
if (myTeams.length === 0) {
|
if (myTeams.length === 0) {
|
||||||
|
if (isUserFirstAdmin) {
|
||||||
|
getHistory().push('/preparing-workspace');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
getHistory().push('/select_team');
|
getHistory().push('/select_team');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6887,6 +6887,15 @@ const AdminDefinition = {
|
|||||||
isHidden: it.licensedForFeature('Cloud'),
|
isHidden: it.licensedForFeature('Cloud'),
|
||||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: Constants.SettingsTypes.TYPE_BOOL,
|
||||||
|
key: 'ExperimentalSettings.DisableRefetchingOnBrowserFocus',
|
||||||
|
label: t('admin.experimental.disableRefetchingOnBrowserFocus.title'),
|
||||||
|
label_default: 'Disable data refetching on browser refocus:',
|
||||||
|
help_text: t('admin.experimental.disableRefetchingOnBrowserFocus.desc'),
|
||||||
|
help_text_default: 'When true, Mattermost will not refetch channels and channel members when the browser regains focus. This may result in improved performance for users with many channels and channel members.',
|
||||||
|
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ export default class GeneratedSetting extends React.PureComponent<Props> {
|
|||||||
private regenerate = (e: React.MouseEvent) => {
|
private regenerate = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.props.onChange(this.props.id, crypto.randomBytes(256).toString('base64').substring(0, 32));
|
// Pure base64 implementation can contain characters that are not URL safe without additional
|
||||||
|
// encoding. Adopt a URL/Filename safer alphabet as noted in https://datatracker.ietf.org/doc/html/rfc4648#section-5
|
||||||
|
// where: 62 - (minus) , 63 _ (underscore)
|
||||||
|
const value = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||||
|
this.props.onChange(this.props.id, value.replaceAll('+', '-').replaceAll('/', '_'));
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default class AppsFormSelectField extends React.PureComponent<Props, Stat
|
|||||||
loadDynamicUserOptions = async (userInput: string): Promise<AppSelectOption[]> => {
|
loadDynamicUserOptions = async (userInput: string): Promise<AppSelectOption[]> => {
|
||||||
const usersSearchResults: UserAutocomplete = await this.props.actions.autocompleteUsers(userInput.toLowerCase());
|
const usersSearchResults: UserAutocomplete = await this.props.actions.autocompleteUsers(userInput.toLowerCase());
|
||||||
|
|
||||||
return usersSearchResults.users.map((user) => {
|
return usersSearchResults.users.filter((user) => !user.is_bot).map((user) => {
|
||||||
const label = this.props.teammateNameDisplay ? displayUsername(user, this.props.teammateNameDisplay) : user.username;
|
const label = this.props.teammateNameDisplay ? displayUsername(user, this.props.teammateNameDisplay) : user.username;
|
||||||
|
|
||||||
return {...user, label, value: user.id, icon_data: imageURLForUser(user.id)};
|
return {...user, label, value: user.id, icon_data: imageURLForUser(user.id)};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {useIntl} from 'react-intl';
|
|||||||
import {useSelector, useDispatch} from 'react-redux';
|
import {useSelector, useDispatch} from 'react-redux';
|
||||||
import {useLocation, useHistory} from 'react-router-dom';
|
import {useLocation, useHistory} from 'react-router-dom';
|
||||||
|
|
||||||
import {redirectUserToDefaultTeam} from 'actions/global_actions';
|
|
||||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||||
|
|
||||||
import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg';
|
import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg';
|
||||||
@@ -15,7 +14,6 @@ import LoadingScreen from 'components/loading_screen';
|
|||||||
|
|
||||||
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
import {clearErrors, logError} from 'mattermost-redux/actions/errors';
|
||||||
import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users';
|
import {verifyUserEmail, getMe} from 'mattermost-redux/actions/users';
|
||||||
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
import {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
@@ -40,7 +38,6 @@ const DoVerifyEmail = () => {
|
|||||||
const token = params.get('token') ?? '';
|
const token = params.get('token') ?? '';
|
||||||
|
|
||||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
|
||||||
|
|
||||||
const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING);
|
const [verifyStatus, setVerifyStatus] = useState(VerifyStatus.PENDING);
|
||||||
const [serverError, setServerError] = useState('');
|
const [serverError, setServerError] = useState('');
|
||||||
@@ -52,16 +49,11 @@ const DoVerifyEmail = () => {
|
|||||||
|
|
||||||
const handleRedirect = () => {
|
const handleRedirect = () => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
if (useCaseOnboarding) {
|
// need info about whether admin or not,
|
||||||
// need info about whether admin or not,
|
// and whether admin has already completed
|
||||||
// and whether admin has already completed
|
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
// let the default root component handle it.
|
||||||
// let the default root component handle it.
|
history.push('/');
|
||||||
history.push('/');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectUserToDefaultTeam();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {withRouter} from 'react-router-dom';
|
|||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getUseCaseOnboarding} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins';
|
import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins';
|
||||||
@@ -32,7 +31,6 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
teamUrl: getCurrentRelativeTeamUrl(state),
|
teamUrl: getCurrentRelativeTeamUrl(state),
|
||||||
pluginMenuItems: getUserGuideDropdownPluginMenuItems(state),
|
pluginMenuItems: getUserGuideDropdownPluginMenuItems(state),
|
||||||
isFirstAdmin: isFirstAdmin(state),
|
isFirstAdmin: isFirstAdmin(state),
|
||||||
useCaseOnboarding: getUseCaseOnboarding(state),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ describe('components/channel_header/components/UserGuideDropdown', () => {
|
|||||||
},
|
},
|
||||||
pluginMenuItems: [],
|
pluginMenuItems: [],
|
||||||
isFirstAdmin: false,
|
isFirstAdmin: false,
|
||||||
useCaseOnboarding: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot', () => {
|
test('should match snapshot', () => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {UserProfile} from '@mattermost/types/users';
|
|||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getUseCaseOnboarding, isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {isGraphQLEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
import {getTeamByName, getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
import {isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||||
@@ -104,7 +104,6 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
const currentUser = useSelector(getCurrentUser);
|
const currentUser = useSelector(getCurrentUser);
|
||||||
const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined));
|
const experimentalPrimaryTeam = useSelector((state: GlobalState) => (ExperimentalPrimaryTeam ? getTeamByName(state, ExperimentalPrimaryTeam) : undefined));
|
||||||
const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? ''));
|
const experimentalPrimaryTeamMember = useSelector((state: GlobalState) => getMyTeamMember(state, experimentalPrimaryTeam?.id ?? ''));
|
||||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
|
||||||
const isCloud = useSelector(isCurrentLicenseCloud);
|
const isCloud = useSelector(isCurrentLicenseCloud);
|
||||||
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
const graphQLEnabled = useSelector(isGraphQLEnabled);
|
||||||
|
|
||||||
@@ -631,14 +630,12 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
} else if (experimentalPrimaryTeamMember.team_id) {
|
} else if (experimentalPrimaryTeamMember.team_id) {
|
||||||
// Only set experimental team if user is on that team
|
// Only set experimental team if user is on that team
|
||||||
history.push(`/${ExperimentalPrimaryTeam}`);
|
history.push(`/${ExperimentalPrimaryTeam}`);
|
||||||
} else if (useCaseOnboarding) {
|
} else {
|
||||||
// need info about whether admin or not,
|
// need info about whether admin or not,
|
||||||
// and whether admin has already completed
|
// and whether admin has already completed
|
||||||
// first time onboarding. Instead of fetching and orchestrating that here,
|
// first time onboarding. Instead of fetching and orchestrating that here,
|
||||||
// let the default root component handle it.
|
// let the default root component handle it.
|
||||||
history.push('/');
|
history.push('/');
|
||||||
} else {
|
|
||||||
redirectUserToDefaultTeam();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`InviteMembers component should match snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="InviteMembers-body test-class"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="SingleColumnLayout"
|
||||||
|
style="width: 547px;"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="PageLine PageLine--no-left"
|
||||||
|
style="margin-bottom: 50px; margin-left: 50px; height: calc(25vh);"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
Previous step
|
||||||
|
</div>
|
||||||
|
<h1
|
||||||
|
class="PreparingWorkspaceTitle"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Invite your team members
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="PreparingWorkspaceDescription"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Collaboration is tough by yourself. Invite a few team members using the invitation link below.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="PreparingWorkspacePageBody"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="InviteMembersLink"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label="team invite link"
|
||||||
|
class="InviteMembersLink__input"
|
||||||
|
data-testid="shareLinkInput"
|
||||||
|
readonly=""
|
||||||
|
type="text"
|
||||||
|
value="https://my-org.mattermost.com/config/signup_user_complete/?id=1234"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="InviteMembersLink__button"
|
||||||
|
data-testid="shareLinkInputButton"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon icon-link-variant"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Copy Link
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="InviteMembers__submit"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="primary-button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Finish setup
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="PageLine PageLine--no-left"
|
||||||
|
style="margin-top: 50px; margin-left: 50px; height: calc(30vh);"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components/preparing-workspace/invite_members_link should match snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="InviteMembersLink"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-label="team invite link"
|
||||||
|
class="InviteMembersLink__input"
|
||||||
|
data-testid="shareLinkInput"
|
||||||
|
readonly=""
|
||||||
|
type="text"
|
||||||
|
value="https://invite-url.mattermost.com"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="InviteMembersLink__button"
|
||||||
|
data-testid="shareLinkInputButton"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon icon-link-variant"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Copy Link
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components/preparing-workspace/organization_status should match snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
class="Organization__status"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
|
|||||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||||
|
|
||||||
import {Action} from 'mattermost-redux/types/actions';
|
import {Action} from 'mattermost-redux/types/actions';
|
||||||
import {checkIfTeamExists, createTeam} from 'mattermost-redux/actions/teams';
|
import {checkIfTeamExists, createTeam, updateTeam} from 'mattermost-redux/actions/teams';
|
||||||
import {getProfiles} from 'mattermost-redux/actions/users';
|
import {getProfiles} from 'mattermost-redux/actions/users';
|
||||||
|
|
||||||
import PreparingWorkspace, {Actions} from './preparing_workspace';
|
import PreparingWorkspace, {Actions} from './preparing_workspace';
|
||||||
@@ -13,6 +13,7 @@ import PreparingWorkspace, {Actions} from './preparing_workspace';
|
|||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
return {
|
return {
|
||||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
||||||
|
updateTeam,
|
||||||
createTeam,
|
createTeam,
|
||||||
getProfiles,
|
getProfiles,
|
||||||
checkIfTeamExists,
|
checkIfTeamExists,
|
||||||
|
|||||||