Make support packet composable with plugins (#26403)

---------

Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
This commit is contained in:
Ibrahim Serdar Acikgoz
2024-04-12 10:05:58 +02:00
committed by GitHub
parent 165b5ea821
commit 92f11f8971
19 changed files with 505 additions and 158 deletions

View File

@@ -1125,6 +1125,25 @@
##### License
Requires either a E10 or E20 license.
operationId: GenerateSupportPacket
parameters:
- name: basic_server_logs
in: query
description: |
Specifies whether the server should include or exclude log files. Default value is true.
__Minimum server version__: 9.8.0
required: false
schema:
type: boolean
- name: plugin_packets
in: query
description: |
Specifies plugin identifiers whose content should be included in the support packet.
__Minimum server version__: 9.8.0
required: false
schema:
type: string
responses:
"400":
$ref: "#/components/responses/BadRequest"

View File

@@ -89,15 +89,28 @@ func generateSupportPacket(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
// We support the existing API hence the logs are always included
// if nothing specified.
includeLogs := true
if r.FormValue("basic_server_logs") == "false" {
includeLogs = false
}
supportPacketOptions := &model.SupportPacketOptions{
IncludeLogs: includeLogs,
PluginPackets: r.Form["plugin_packets"],
}
// Checking to see if the server has a e10 or e20 license (this feature is only permitted for servers with licenses)
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("Api4.generateSupportPacket", "api.no_license", nil, "", http.StatusForbidden)
return
}
fileDatas := c.App.GenerateSupportPacket(c.AppContext)
fileDatas := c.App.GenerateSupportPacket(c.AppContext, supportPacketOptions)
// Constructing the ZIP file name as per spec (mattermost_support_packet_YYYY-MM-DD-HH-MM.zip)
// Note that this filename is also being checked at the webapp, please update the
// regex within the commercial_support_modal.tsx file if the naming convention ever changes.
now := time.Now()
outputZipFilename := fmt.Sprintf("mattermost_support_packet_%s.zip", now.Format("2006-01-02-03-04"))

View File

@@ -618,7 +618,7 @@ type AppIface interface {
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
GeneratePresignURLForExport(name string) (*model.PresignURLResponse, *model.AppError)
GeneratePublicLink(siteURL string, info *model.FileInfo) string
GenerateSupportPacket(c request.CTX) []model.FileData
GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData
GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError)
GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError)
GetActivePluginManifests() ([]*model.Manifest, *model.AppError)

View File

@@ -4786,7 +4786,7 @@ func (a *OpenTracingAppLayer) GeneratePublicLink(siteURL string, info *model.Fil
return resultVar0
}
func (a *OpenTracingAppLayer) GenerateSupportPacket(c request.CTX) []model.FileData {
func (a *OpenTracingAppLayer) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateSupportPacket")
@@ -4798,7 +4798,7 @@ func (a *OpenTracingAppLayer) GenerateSupportPacket(c request.CTX) []model.FileD
}()
defer span.Finish()
resultVar0 := a.app.GenerateSupportPacket(c)
resultVar0 := a.app.GenerateSupportPacket(c, options)
return resultVar0
}

View File

@@ -26,7 +26,7 @@ const (
cpuProfileDuration = 5 * time.Second
)
func (a *App) GenerateSupportPacket(c request.CTX) []model.FileData {
func (a *App) GenerateSupportPacket(c request.CTX, options *model.SupportPacketOptions) []model.FileData {
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
var warnings []string
@@ -35,14 +35,17 @@ func (a *App) GenerateSupportPacket(c request.CTX) []model.FileData {
// A array of the functions that we can iterate through since they all have the same return value
functions := map[string]func(c request.CTX) (*model.FileData, error){
"support package": a.generateSupportPacketYaml,
"plugins": a.createPluginsFile,
"config": a.createSanitizedConfigFile,
"mattermost log": a.getMattermostLog,
"notification log": a.getNotificationsLog,
"cpu profile": a.createCPUProfile,
"heap profile": a.createHeapProfile,
"goroutines": a.createGoroutineProfile,
"support package": a.generateSupportPacketYaml,
"plugins": a.createPluginsFile,
"config": a.createSanitizedConfigFile,
"cpu profile": a.createCPUProfile,
"heap profile": a.createHeapProfile,
"goroutines": a.createGoroutineProfile,
}
if options.IncludeLogs {
functions["mattermost log"] = a.getMattermostLog
functions["notification log"] = a.getNotificationsLog
}
for name, fn := range functions {
@@ -57,6 +60,27 @@ func (a *App) GenerateSupportPacket(c request.CTX) []model.FileData {
}
}
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
pluginContext := pluginContext(c)
for _, id := range options.PluginPackets {
hooks, err := pluginsEnvironment.HooksForPlugin(id)
if err != nil {
c.Logger().Error("Failed to call hooks for plugin", mlog.Err(err), mlog.String("plugin", id))
warnings = append(warnings, err.Error())
continue
}
pluginData, err := hooks.GenerateSupportData(pluginContext)
if err != nil {
c.Logger().Warn("Failed to generate plugin file for support package", mlog.Err(err), mlog.String("plugin", id))
warnings = append(warnings, err.Error())
continue
}
for _, data := range pluginData {
fileDatas = append(fileDatas, *data)
}
}
}
// Adding a warning.txt file to the fileDatas if any warning
if len(warnings) > 0 {
finalWarning := strings.Join(warnings, "\n")

View File

@@ -168,55 +168,91 @@ func TestGenerateSupportPacket(t *testing.T) {
logLocation := config.GetLogFileLocation(dir)
notificationsLogLocation := config.GetNotificationsLogFileLocation(dir)
d1 := []byte("hello\ngo\n")
err = os.WriteFile(logLocation, d1, 0777)
require.NoError(t, err)
err = os.WriteFile(notificationsLogLocation, d1, 0777)
require.NoError(t, err)
fileDatas := th.App.GenerateSupportPacket(th.Context)
var rFileNames []string
testFiles := []string{
"support_packet.yaml",
"plugins.json",
"sanitized_config.json",
"mattermost.log",
"notifications.log",
"cpu.prof",
"heap.prof",
"goroutines",
genMockLogFiles := func() {
d1 := []byte("hello\ngo\n")
genErr := os.WriteFile(logLocation, d1, 0777)
require.NoError(t, genErr)
genErr = os.WriteFile(notificationsLogLocation, d1, 0777)
require.NoError(t, genErr)
}
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
genMockLogFiles()
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
t.Run("generate support packet with logs", func(t *testing.T) {
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
var rFileNames []string
testFiles := []string{
"support_packet.yaml",
"plugins.json",
"sanitized_config.json",
"mattermost.log",
"notifications.log",
"cpu.prof",
"heap.prof",
"goroutines",
}
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
// Remove these two files and ensure that warning.txt file is generated
err = os.Remove(logLocation)
require.NoError(t, err)
err = os.Remove(notificationsLogLocation)
require.NoError(t, err)
fileDatas = th.App.GenerateSupportPacket(th.Context)
testFiles = []string{
"support_packet.yaml",
"plugins.json",
"sanitized_config.json",
"cpu.prof",
"heap.prof",
"warning.txt",
"goroutines",
}
rFileNames = nil
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
})
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
t.Run("generate support packet without logs", func(t *testing.T) {
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
testFiles := []string{
"support_packet.yaml",
"plugins.json",
"sanitized_config.json",
"cpu.prof",
"heap.prof",
"goroutines",
}
var rFileNames []string
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
})
t.Run("remove the log files and ensure that warning.txt file is generated", func(t *testing.T) {
// Remove these two files and ensure that warning.txt file is generated
err = os.Remove(logLocation)
require.NoError(t, err)
err = os.Remove(notificationsLogLocation)
require.NoError(t, err)
t.Cleanup(genMockLogFiles)
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: true,
})
testFiles := []string{
"support_packet.yaml",
"plugins.json",
"sanitized_config.json",
"cpu.prof",
"heap.prof",
"warning.txt",
"goroutines",
}
var rFileNames []string
for _, fileData := range fileDatas {
require.NotNil(t, fileData)
assert.Positive(t, len(fileData.Body))
rFileNames = append(rFileNames, fileData.Filename)
}
assert.ElementsMatch(t, testFiles, rFileNames)
})
t.Run("steps that generated an error should still return file data", func(t *testing.T) {
mockStore := smocks.Store{}
@@ -241,7 +277,9 @@ func TestGenerateSupportPacket(t *testing.T) {
mockStore.On("GetDbVersion", false).Return("1.0.0", nil)
th.App.Srv().SetStore(&mockStore)
fileDatas := th.App.GenerateSupportPacket(th.Context)
fileDatas := th.App.GenerateSupportPacket(th.Context, &model.SupportPacketOptions{
IncludeLogs: false,
})
var rFileNames []string
for _, fileData := range fileDatas {

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
type SupportPacket struct {
/* Build information */
ServerOS string `yaml:"server_os"`
ServerArchitecture string `yaml:"server_architecture"`
ServerVersion string `yaml:"server_version"`
BuildHash string `yaml:"build_hash"`
/* DB */
DatabaseType string `yaml:"database_type"`
DatabaseVersion string `yaml:"database_version"`
DatabaseSchemaVersion string `yaml:"database_schema_version"`
WebsocketConnections int `yaml:"websocket_connections"`
MasterDbConnections int `yaml:"master_db_connections"`
ReplicaDbConnections int `yaml:"read_db_connections"`
/* Cluster */
ClusterID string `yaml:"cluster_id"`
/* File store */
FileDriver string `yaml:"file_driver"`
FileStatus string `yaml:"file_status"`
/* LDAP */
LdapVendorName string `yaml:"ldap_vendor_name,omitempty"`
LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"`
/* Elastic Search */
ElasticServerVersion string `yaml:"elastic_server_version,omitempty"`
ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"`
/* License */
LicenseTo string `yaml:"license_to"`
LicenseSupportedUsers int `yaml:"license_supported_users"`
LicenseIsTrial bool `yaml:"license_is_trial,omitempty"`
/* Server stats */
ActiveUsers int `yaml:"active_users"`
DailyActiveUsers int `yaml:"daily_active_users"`
MonthlyActiveUsers int `yaml:"monthly_active_users"`
InactiveUserCount int `yaml:"inactive_user_count"`
TotalPosts int `yaml:"total_posts"`
TotalChannels int `yaml:"total_channels"`
TotalTeams int `yaml:"total_teams"`
/* Jobs */
DataRetentionJobs []*Job `yaml:"data_retention_jobs"`
MessageExportJobs []*Job `yaml:"message_export_jobs"`
ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"`
ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"`
BlevePostIndexingJobs []*Job `yaml:"bleve_post_indexin_jobs"`
LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"`
MigrationJobs []*Job `yaml:"migration_jobs"`
}
type FileData struct {
Filename string
Body []byte
}
type SupportPacketOptions struct {
IncludeLogs bool `json:"include_logs"` // IncludeLogs is the option to include server logs
PluginPackets []string `json:"plugin_packets"` // PluginPackets is a list of pluginids to call hooks
}
// SupportPacketOptionsFromReader decodes a json-encoded request from the given io.Reader.
func SupportPacketOptionsFromReader(reader io.Reader) (*SupportPacketOptions, error) {
var r *SupportPacketOptions
err := json.NewDecoder(reader).Decode(&r)
if err != nil {
return nil, err
}
return r, nil
}

View File

@@ -78,73 +78,6 @@ type ServerBusyState struct {
ExpiresTS string `json:"expires_ts,omitempty"`
}
type SupportPacket struct {
/* Build information */
ServerOS string `yaml:"server_os"`
ServerArchitecture string `yaml:"server_architecture"`
ServerVersion string `yaml:"server_version"`
BuildHash string `yaml:"build_hash"`
/* DB */
DatabaseType string `yaml:"database_type"`
DatabaseVersion string `yaml:"database_version"`
DatabaseSchemaVersion string `yaml:"database_schema_version"`
WebsocketConnections int `yaml:"websocket_connections"`
MasterDbConnections int `yaml:"master_db_connections"`
ReplicaDbConnections int `yaml:"read_db_connections"`
/* Cluster */
ClusterID string `yaml:"cluster_id"`
/* File store */
FileDriver string `yaml:"file_driver"`
FileStatus string `yaml:"file_status"`
/* LDAP */
LdapVendorName string `yaml:"ldap_vendor_name,omitempty"`
LdapVendorVersion string `yaml:"ldap_vendor_version,omitempty"`
/* Elastic Search */
ElasticServerVersion string `yaml:"elastic_server_version,omitempty"`
ElasticServerPlugins []string `yaml:"elastic_server_plugins,omitempty"`
/* License */
LicenseTo string `yaml:"license_to"`
LicenseSupportedUsers int `yaml:"license_supported_users"`
LicenseIsTrial bool `yaml:"license_is_trial,omitempty"`
/* Server stats */
ActiveUsers int `yaml:"active_users"`
DailyActiveUsers int `yaml:"daily_active_users"`
MonthlyActiveUsers int `yaml:"monthly_active_users"`
InactiveUserCount int `yaml:"inactive_user_count"`
TotalPosts int `yaml:"total_posts"`
TotalChannels int `yaml:"total_channels"`
TotalTeams int `yaml:"total_teams"`
/* Jobs */
DataRetentionJobs []*Job `yaml:"data_retention_jobs"`
MessageExportJobs []*Job `yaml:"message_export_jobs"`
ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"`
ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"`
BlevePostIndexingJobs []*Job `yaml:"bleve_post_indexin_jobs"`
LdapSyncJobs []*Job `yaml:"ldap_sync_jobs"`
MigrationJobs []*Job `yaml:"migration_jobs"`
}
type FileData struct {
Filename string
Body []byte
}
type AppliedMigration struct {
Version int `json:"version"`
Name string `json:"name"`

View File

@@ -1125,6 +1125,42 @@ func (s *hooksRPCServer) OnSharedChannelsProfileImageSyncMsg(args *Z_OnSharedCha
return nil
}
func init() {
hookNameToId["GenerateSupportData"] = GenerateSupportDataID
}
type Z_GenerateSupportDataArgs struct {
A *Context
}
type Z_GenerateSupportDataReturns struct {
A []*model.FileData
B error
}
func (g *hooksRPCClient) GenerateSupportData(c *Context) ([]*model.FileData, error) {
_args := &Z_GenerateSupportDataArgs{c}
_returns := &Z_GenerateSupportDataReturns{}
if g.implemented[GenerateSupportDataID] {
if err := g.client.Call("Plugin.GenerateSupportData", _args, _returns); err != nil {
g.log.Error("RPC call GenerateSupportData to plugin failed.", mlog.Err(err))
}
}
return _returns.A, _returns.B
}
func (s *hooksRPCServer) GenerateSupportData(args *Z_GenerateSupportDataArgs, returns *Z_GenerateSupportDataReturns) error {
if hook, ok := s.impl.(interface {
GenerateSupportData(c *Context) ([]*model.FileData, error)
}); ok {
returns.A, returns.B = hook.GenerateSupportData(args.A)
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("Hook GenerateSupportData called but not implemented."))
}
return nil
}
type Z_RegisterCommandArgs struct {
A *model.Command
}

View File

@@ -60,6 +60,7 @@ const (
PreferencesHaveChangedID = 42
OnSharedChannelsAttachmentSyncMsgID = 43
OnSharedChannelsProfileImageSyncMsgID = 44
GenerateSupportDataID = 45
TotalHooksID = iota
)
@@ -382,4 +383,10 @@ type Hooks interface {
//
// Minimum server version: 9.5
OnSharedChannelsProfileImageSyncMsg(user *model.User, rc *model.RemoteCluster) error
// GenerateSupportData is invoked when a Support Packet gets generated.
// It allows plugins to include their own content in the Support Packet.
//
// Minimum server version: 9.8
GenerateSupportData(c *Context) ([]*model.FileData, error)
}

View File

@@ -284,3 +284,10 @@ func (hooks *hooksTimerLayer) OnSharedChannelsProfileImageSyncMsg(user *model.Us
hooks.recordTime(startTime, "OnSharedChannelsProfileImageSyncMsg", _returnsA == nil)
return _returnsA
}
func (hooks *hooksTimerLayer) GenerateSupportData(c *Context) ([]*model.FileData, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := hooks.hooksImpl.GenerateSupportData(c)
hooks.recordTime(startTime, "GenerateSupportData", _returnsB == nil)
return _returnsA, _returnsB
}

View File

@@ -105,6 +105,32 @@ func (_m *Hooks) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, fil
return r0, r1
}
// GenerateSupportData provides a mock function with given fields: c
func (_m *Hooks) GenerateSupportData(c *plugin.Context) ([]*model.FileData, error) {
ret := _m.Called(c)
var r0 []*model.FileData
var r1 error
if rf, ok := ret.Get(0).(func(*plugin.Context) ([]*model.FileData, error)); ok {
return rf(c)
}
if rf, ok := ret.Get(0).(func(*plugin.Context) []*model.FileData); ok {
r0 = rf(c)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.FileData)
}
}
if rf, ok := ret.Get(1).(func(*plugin.Context) error); ok {
r1 = rf(c)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Implemented provides a mock function with given fields:
func (_m *Hooks) Implemented() ([]string, error) {
ret := _m.Called()

View File

@@ -52,11 +52,7 @@ exports[`components/CommercialSupportModal should match snapshot 1`] = `
className="CommercialSupportModal"
>
<FormattedMarkdownMessage
defaultMessage="If you're experiencing issues, [submit a support ticket.](!{supportLink})
**Download Support Packet**
We recommend that you download additional environment details about your Mattermost environment to help with troubleshooting. Once downloaded, attach the packet to your support ticket to share with our Customer Support team."
defaultMessage="If you're experiencing issues, [submit a support ticket](!{supportLink}). To help with troubleshooting, it's recommended to download the Support Packet below that includes more details about your Mattermost environment."
id="commercial_support.description"
values={
Object {
@@ -64,26 +60,41 @@ We recommend that you download additional environment details about your Matterm
}
}
/>
<a
className="btn btn-primary DownloadSupportPacket"
href="/api/v4/system/support_packet"
rel="noopener noreferrer"
>
<MemoizedFormattedMessage
defaultMessage="Download Support Packet"
id="commercial_support.download_support_packet"
/>
</a>
<AlertBanner
message={
<FormattedMarkdownMessage
defaultMessage="Before downloading the support packet, set **Output Logs to File** to **true** and set **File Log Level** to **DEBUG** [here](!/admin_console/environment/logging)."
defaultMessage="Before downloading the Support Packet, set **Output Logs to File** to **true** and set **File Log Level** to **DEBUG** [here](!/admin_console/environment/logging)."
id="commercial_support.warning.banner"
/>
}
mode="info"
onDismiss={[Function]}
/>
<div
className="CommercialSupportModal__packet_contents_download"
>
<FormattedMarkdownMessage
defaultMessage="**Select your Support Packet contents to download**"
id="commercial_support.download_contents"
/>
</div>
<div
className="CommercialSupportModal__download"
>
<a
className="btn btn-primary DownloadSupportPacket"
onClick={[Function]}
rel="noopener noreferrer"
>
<i
className="icon icon-download-outline"
/>
<MemoizedFormattedMessage
defaultMessage="Download Support Packet"
id="commercial_support.download_support_packet"
/>
</a>
</div>
</div>
</ModalBody>
</Modal>

View File

@@ -1,5 +1,5 @@
.CommercialSupportModal {
padding: 20px 20px;
padding: 0 32px 32px 32px;
.DownloadSupportPacket {
margin-top: 20px;
@@ -8,4 +8,19 @@
.AlertBanner {
margin-top: 20px;
}
&__packet_contents_download {
margin-top: 20px;
margin-bottom: 12px;
}
&__options_checkbox_label {
padding-left: 12px;
font-weight: 500;
}
}
.modal-header .modal-title {
font-family: Metropolis, sans-serif;
font-weight: 600;
}

View File

@@ -17,6 +17,7 @@ describe('components/CommercialSupportModal', () => {
showBannerWarning={true}
isCloud={false}
currentUser={mockUser}
packetContents={[]}
/>,
);
expect(wrapper).toMatchSnapshot();

View File

@@ -1,16 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import moment from 'moment';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import type {SupportPacketContent} from '@mattermost/types/admin';
import type {UserProfile} from '@mattermost/types/users';
import {Client4} from 'mattermost-redux/client';
import AlertBanner from 'components/alert_banner';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import './commercial_support_modal.scss';
@@ -26,20 +29,25 @@ type Props = {
isCloud: boolean;
currentUser: UserProfile;
packetContents: SupportPacketContent[];
};
type State = {
show: boolean;
showBannerWarning: boolean;
packetContents: SupportPacketContent[];
loading: boolean;
};
export default class CommercialSupportModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
show: true,
showBannerWarning: props.showBannerWarning,
packetContents: props.packetContents,
loading: false,
};
}
@@ -61,6 +69,59 @@ export default class CommercialSupportModal extends React.PureComponent<Props, S
this.updateBannerWarning(false);
};
updateCheckStatus = (index: number) => {
this.setState({
packetContents: this.state.packetContents.map((content, currentIndex) => (
(currentIndex === index && !content.mandatory) ? {...content, selected: !content.selected} : content
)),
});
};
genereateDownloadURLWithParams = (): string => {
const url = new URL(Client4.getSystemRoute() + '/support_packet');
this.state.packetContents.forEach((content) => {
if (content.id === 'basic.server.logs') {
url.searchParams.set('basic_server_logs', String(content.selected));
} else if (!content.mandatory && content.selected) {
url.searchParams.append('plugin_packets', content.id);
}
});
return url.toString();
};
extractFilename = (input: string | null): string => {
// construct the expected filename in case of an error in the header
const formattedDate = (moment(new Date())).format('YYYY-MM-DD-HH-mm');
const presumedFileName = `mattermost_support_packet_${formattedDate}.zip`;
if (input === null) {
return presumedFileName;
}
const regex = /filename\*?=["']?((?:\\.|[^"'\s])+)(?=["']?)/g;
const matches = regex.exec(input!);
return matches ? matches[1] : presumedFileName;
};
downloadSupportPacket = async () => {
this.setState({loading: true});
const res = await fetch(this.genereateDownloadURLWithParams(), {
method: 'GET',
headers: {'Content-Type': 'application/zip'},
});
const blob = await res.blob();
this.setState({loading: false});
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', this.extractFilename(res.headers.get('content-disposition')));
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
render() {
const {showBannerWarning} = this.state;
const {isCloud, currentUser} = this.props;
@@ -86,33 +147,71 @@ export default class CommercialSupportModal extends React.PureComponent<Props, S
<div className='CommercialSupportModal'>
<FormattedMarkdownMessage
id='commercial_support.description'
defaultMessage={'If you\'re experiencing issues, [submit a support ticket.](!{supportLink})\n \n**Download Support Packet**\n \nWe recommend that you download additional environment details about your Mattermost environment to help with troubleshooting. Once downloaded, attach the packet to your support ticket to share with our Customer Support team.'}
defaultMessage={'If you\'re experiencing issues, [submit a support ticket](!{supportLink}). To help with troubleshooting, it\'s recommended to download the Support Packet below that includes more details about your Mattermost environment.'}
values={{
supportLink,
}}
/>
<a
className='btn btn-primary DownloadSupportPacket'
href={`${Client4.getBaseRoute()}/system/support_packet`}
rel='noopener noreferrer'
>
<FormattedMessage
id='commercial_support.download_support_packet'
defaultMessage='Download Support Packet'
/>
</a>
{showBannerWarning &&
<AlertBanner
mode='info'
message={
<FormattedMarkdownMessage
id='commercial_support.warning.banner'
defaultMessage='Before downloading the support packet, set **Output Logs to File** to **true** and set **File Log Level** to **DEBUG** [here](!/admin_console/environment/logging).'
defaultMessage='Before downloading the Support Packet, set **Output Logs to File** to **true** and set **File Log Level** to **DEBUG** [here](!/admin_console/environment/logging).'
/>
}
onDismiss={this.hideBannerWarning}
/>
}
<div className='CommercialSupportModal__packet_contents_download'>
<FormattedMarkdownMessage
id='commercial_support.download_contents'
defaultMessage={'**Select your Support Packet contents to download**'}
/>
</div>
{this.state.packetContents.map((item, index) => (
<div
className='CommercialSupportModal__option'
key={item.id}
>
<input
className='CommercialSupportModal__options__checkbox'
id={item.id}
name={item.id}
type='checkbox'
checked={item.selected}
disabled={item.mandatory}
onChange={() => this.updateCheckStatus(index)}
/>
<FormattedMessage
id='mettormost.plugin.metrics.support.packet'
defaultMessage={item.label}
>
{(text) => (
<label
className='CommercialSupportModal__options_checkbox_label'
htmlFor={item.id}
>
{text}
</label>)
}
</FormattedMessage>
</div>
))}
<div className='CommercialSupportModal__download'>
<a
className='btn btn-primary DownloadSupportPacket'
onClick={this.downloadSupportPacket}
rel='noopener noreferrer'
>
{ this.state.loading ? <LoadingSpinner/> : <i className='icon icon-download-outline'/> }
<FormattedMessage
id='commercial_support.download_support_packet'
defaultMessage='Download Support Packet'
/>
</a>
</div>
</div>
</Modal.Body>
</Modal>

View File

@@ -16,11 +16,27 @@ function mapStateToProps(state: GlobalState) {
const isCloud = license.Cloud === 'true';
const currentUser = getCurrentUser(state);
const showBannerWarning = (config.EnableFile !== 'true' || config.FileLevel !== 'DEBUG') && !(isCloud);
const packetContents = [
{id: 'basic.contents', label: 'Basic contents', selected: true, mandatory: true},
{id: 'basic.server.logs', label: 'Server logs', selected: true, mandatory: false},
];
for (const [key, value] of Object.entries(state.entities.admin.plugins!)) {
if (value.active && value.props !== undefined && value.props.support_packet !== undefined) {
packetContents.push({
id: key,
label: value.props.support_packet,
selected: false,
mandatory: false,
});
}
}
return {
isCloud,
currentUser,
showBannerWarning,
packetContents,
};
}

View File

@@ -3345,10 +3345,11 @@
"combined_system_message.removed_from_team.one_you": "You were **removed from the team**.",
"combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.",
"combined_system_message.you": "You",
"commercial_support.description": "If you're experiencing issues, [submit a support ticket.](!{supportLink})\n \n**Download Support Packet**\n \nWe recommend that you download additional environment details about your Mattermost environment to help with troubleshooting. Once downloaded, attach the packet to your support ticket to share with our Customer Support team.",
"commercial_support.description": "If you're experiencing issues, [submit a support ticket](!{supportLink}). To help with troubleshooting, it's recommended to download the support packet below that includes more details about your Mattermost environment.",
"commercial_support.download_contents": "**Select your Support Packet contents to download**",
"commercial_support.download_support_packet": "Download Support Packet",
"commercial_support.title": "Commercial Support",
"commercial_support.warning.banner": "Before downloading the support packet, set **Output Logs to File** to **true** and set **File Log Level** to **DEBUG** [here](!/admin_console/environment/logging).",
"commercial_support.warning.banner": "Before downloading the Support Packet, set **Output Logs to File** to **true** and set **File Log Level** to **DEBUG** [here](!/admin_console/environment/logging).",
"confirm_modal.cancel": "Cancel",
"confirm_switch_to_yearly_modal.confirm": "Confirm",
"confirm_switch_to_yearly_modal.contact_sales": "Contact Sales",

View File

@@ -125,3 +125,11 @@ export type SchemaMigration = {
version: number;
name: string;
};
export type SupportPacketContent = {
id: string;
translation_id?: string;
label: string;
selected: boolean;
mandatory: boolean;
}