mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Make support packet composable with plugins (#26403)
--------- Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
This commit is contained in:
committed by
GitHub
parent
165b5ea821
commit
92f11f8971
@@ -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"
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
93
server/public/model/support_packet.go
Normal file
93
server/public/model/support_packet.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('components/CommercialSupportModal', () => {
|
||||
showBannerWarning={true}
|
||||
isCloud={false}
|
||||
currentUser={mockUser}
|
||||
packetContents={[]}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user