diff --git a/api/v4/source/system.yaml b/api/v4/source/system.yaml
index 8b1e5a59dc43..1bf3208dca1e 100644
--- a/api/v4/source/system.yaml
+++ b/api/v4/source/system.yaml
@@ -1013,20 +1013,9 @@
No permission required but having the `manage_system` permission returns more information.
operationId: GetClientLicense
- parameters:
- - name: format
- in: query
- required: true
- description: Must be `old`, other formats not implemented yet
- schema:
- type: string
responses:
"200":
description: License retrieval successful
- "400":
- $ref: "#/components/responses/BadRequest"
- "501":
- $ref: "#/components/responses/NotImplemented"
/api/v4/license/load_metric:
get:
tags:
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts
index fe8c2eb00def..5dfb4b39c86c 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_pricing_modal_spec.ts
@@ -183,7 +183,7 @@ describe('Self hosted Pricing modal', () => {
}
function withTrialLicense(trial: string) {
- cy.intercept('GET', '**/api/v4/license/client?format=old', {
+ cy.intercept('GET', '**/api/v4/license/client', {
statusCode: 200,
body: {
IsLicensed: 'true',
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js
index f441ef05c981..cf869bb62e9c 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/license_preview_spec.js
@@ -22,7 +22,7 @@ import * as TIMEOUTS from '@/fixtures/timeouts';
const OLD_LICENSE_ID = 'old-license-id-0000000000000';
-// Current (already applied) license, as returned by GET /license/client?format=old.
+// Current (already applied) license, as returned by GET /license/client.
const currentClientLicense = buildClientLicense({
id: OLD_LICENSE_ID,
skuName: 'Professional',
@@ -57,7 +57,7 @@ describe('System console - License preview/diff view', () => {
// mutable holder so we can simulate the server only reporting the new
// license once propagation completes (by the time the modal closes).
const clientLicenseHolder = {body: currentClientLicense};
- cy.intercept('GET', '**/api/v4/license/client?format=old', (req) => {
+ cy.intercept('GET', '**/api/v4/license/client', (req) => {
req.reply({statusCode: 200, body: clientLicenseHolder.body});
}).as('getClientLicense');
@@ -121,7 +121,7 @@ describe('System console - License preview/diff view', () => {
it('MM-67113 - Warns when re-uploading the currently applied license and leaves it unchanged', () => {
// # The displayed license stays Professional throughout this flow
- cy.intercept('GET', '**/api/v4/license/client?format=old', {
+ cy.intercept('GET', '**/api/v4/license/client', {
statusCode: 200,
body: currentClientLicense,
}).as('getClientLicense');
@@ -222,7 +222,7 @@ function buildLicense({id, skuName, skuShortName, users}) {
};
}
-// Build a ClientLicense object (old format) matching GET /license/client?format=old.
+// Build a ClientLicense object (old format) matching GET /license/client.
// All values are strings, as produced by the server.
function buildClientLicense({id, skuName, skuShortName, users}) {
const now = Date.now();
diff --git a/e2e-tests/cypress/tests/support/api/system.js b/e2e-tests/cypress/tests/support/api/system.js
index 68b3b38ce086..320758bbcfc4 100644
--- a/e2e-tests/cypress/tests/support/api/system.js
+++ b/e2e-tests/cypress/tests/support/api/system.js
@@ -27,7 +27,7 @@ function hasLicenseForFeature(license, key) {
}
Cypress.Commands.add('apiGetClientLicense', () => {
- return cy.request('/api/v4/license/client?format=old').then((response) => {
+ return cy.request('/api/v4/license/client').then((response) => {
expect(response.status).to.equal(200);
const license = response.body;
diff --git a/server/channels/api4/license.go b/server/channels/api4/license.go
index ac89f0d4312b..37c5ebfda05a 100644
--- a/server/channels/api4/license.go
+++ b/server/channels/api4/license.go
@@ -28,18 +28,6 @@ func (api *API) InitLicense() {
}
func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
- format := r.URL.Query().Get("format")
-
- if format == "" {
- c.Err = model.NewAppError("getClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
- return
- }
-
- if format != "old" {
- c.SetInvalidParam("format")
- return
- }
-
var clientLicense map[string]string
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
diff --git a/server/channels/api4/license_local.go b/server/channels/api4/license_local.go
index 08bd74dbcc71..524d84e32ee9 100644
--- a/server/channels/api4/license_local.go
+++ b/server/channels/api4/license_local.go
@@ -103,18 +103,6 @@ func localRemoveLicense(c *Context, w http.ResponseWriter, r *http.Request) {
}
func localGetClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
- format := r.URL.Query().Get("format")
-
- if format == "" {
- c.Err = model.NewAppError("localGetClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
- return
- }
-
- if format != "old" {
- c.SetInvalidParam("format")
- return
- }
-
clientLicense := c.App.Srv().ClientLicense()
if _, err := w.Write([]byte(model.MapToJSON(clientLicense))); err != nil {
diff --git a/server/channels/api4/license_test.go b/server/channels/api4/license_test.go
index 61ab4407b360..f15f2bca489f 100644
--- a/server/channels/api4/license_test.go
+++ b/server/channels/api4/license_test.go
@@ -38,14 +38,12 @@ func TestGetOldClientLicense(t *testing.T) {
require.NoError(t, err)
resp, err := client.DoAPIGet(context.Background(), "/license/client", "")
- require.Error(t, err, "get /license/client did not return an error")
- require.Equal(t, http.StatusBadRequest, resp.StatusCode,
- "expected 400 bad request")
+ require.NoError(t, err, "get /license/client should not return an error")
+ require.Equal(t, http.StatusOK, resp.StatusCode, "expected 200 OK")
resp, err = client.DoAPIGet(context.Background(), "/license/client?format=junk", "")
- require.Error(t, err, "get /license/client?format=junk did not return an error")
- require.Equal(t, http.StatusBadRequest, resp.StatusCode,
- "expected 400 Bad Request")
+ require.NoError(t, err, "get /license/client?format=junk should not return an error")
+ require.Equal(t, http.StatusOK, resp.StatusCode, "expected 200 OK")
license, _, err = th.SystemAdminClient.GetOldClientLicense(context.Background(), "")
require.NoError(t, err)
diff --git a/server/i18n/en.json b/server/i18n/en.json
index ae2b6842a3ed..9b6bfc1ab1c4 100644
--- a/server/i18n/en.json
+++ b/server/i18n/en.json
@@ -2700,10 +2700,6 @@
"id": "api.license.add_license.wrong_environment_test.app_error",
"translation": "This is a test or development license, but this server is running in a production environment. Test or development licenses can only be used on test or development servers."
},
- {
- "id": "api.license.client.old_format.app_error",
- "translation": "New format for the client license is not supported yet. Please specify format=old in the query string."
- },
{
"id": "api.license.load_metric.app_error",
"translation": "Failed to compute monthly active users."
diff --git a/webapp/channels/src/components/integrations/bots/bot.test.tsx b/webapp/channels/src/components/integrations/bots/bot.test.tsx
index cef091134254..3fc4147c679d 100644
--- a/webapp/channels/src/components/integrations/bots/bot.test.tsx
+++ b/webapp/channels/src/components/integrations/bots/bot.test.tsx
@@ -296,4 +296,30 @@ describe('components/integrations/bots/Bot', () => {
expect(screen.queryByText(/^Disable$/)).not.toBeInTheDocument();
expect(screen.getByText(/^Enable$/)).toBeInTheDocument();
});
+
+ it('shows a copy button for a newly created token secret', async () => {
+ const bot = UtilsTestHelper.getBotMock({user_id: '1', owner_id: '1'});
+ const owner = UtilsTestHelper.getUserMock({id: bot.owner_id});
+ const user = UtilsTestHelper.getUserMock({id: bot.user_id});
+ const createUserAccessToken = jest.fn().mockResolvedValue({data: {id: 'new-token-id', description: 'bot token', token: 'bot-secret'}});
+
+ renderWithContext(
+ ,
+ );
+
+ fireEvent.click(screen.getByText('Create New Token'));
+ fireEvent.change(screen.getByLabelText('Token Description:'), {target: {value: 'bot token'}});
+ fireEvent.click(screen.getByText('Save'));
+
+ expect(await screen.findByText(/bot-secret/)).toBeInTheDocument();
+ expect(screen.getByLabelText('Copy Token')).toBeInTheDocument();
+ });
});
diff --git a/webapp/channels/src/components/integrations/bots/bot.tsx b/webapp/channels/src/components/integrations/bots/bot.tsx
index 4a6e5666585e..25cc22916bc6 100644
--- a/webapp/channels/src/components/integrations/bots/bot.tsx
+++ b/webapp/channels/src/components/integrations/bots/bot.tsx
@@ -3,7 +3,7 @@
import React from 'react';
import type {ChangeEvent, SyntheticEvent, ReactNode} from 'react';
-import {FormattedMessage} from 'react-intl';
+import {defineMessage, FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {Button} from '@mattermost/shared/components/button';
@@ -14,12 +14,15 @@ import type {UserProfile, UserAccessToken} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
import ConfirmModal from 'components/confirm_modal';
+import CopyText from 'components/copy_text';
import Markdown from 'components/markdown';
import SaveButton from 'components/save_button';
import WarningIcon from 'components/widgets/icons/fa_warning_icon';
import * as Utils from 'utils/utils';
+const copyTokenMessage = defineMessage({id: 'integrations.copy_token', defaultMessage: 'Copy Token'});
+
export function matchesFilter(bot: BotType, filter?: string, owner?: UserProfile): boolean {
if (!filter) {
return true;
@@ -471,6 +474,10 @@ export default class Bot extends React.PureComponent {
/>
{this.state.token.token}
+
) : (
-
-
+
+
+ {state.newToken!.token}
+
+
- {state.newToken!.token}
-
+ >
)}
),
@@ -1130,6 +1138,10 @@ class UserAccessTokenSection extends React.PureComponent {
/>
{this.state.newToken!.token}
+
);
} else {
diff --git a/webapp/channels/src/sass/components/_post.scss b/webapp/channels/src/sass/components/_post.scss
index 7a7dd08cfca1..02e47161e669 100644
--- a/webapp/channels/src/sass/components/_post.scss
+++ b/webapp/channels/src/sass/components/_post.scss
@@ -1770,11 +1770,18 @@
.post__img {
width: 24px;
height: 24px;
+ flex: 0 0 auto;
padding: 0;
text-align: left;
img.avatar-post-preview {
display: block;
+
+ // The shared avatar rules set min-width: 0, which lets the image
+ // shrink (and look squished) inside constrained flex layouts.
+ // Pin it to its intended size so it always stays round.
+ min-width: 24px;
+ flex-shrink: 0;
}
}
diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts
index e621332a877e..aad9668d2704 100644
--- a/webapp/platform/client/src/client4.ts
+++ b/webapp/platform/client/src/client4.ts
@@ -3068,7 +3068,7 @@ export default class Client4 {
getClientLicenseOld = () => {
return this.doFetch(
- `${this.getBaseRoute()}/license/client?format=old`,
+ `${this.getBaseRoute()}/license/client`,
{method: 'get'},
);
};