PLT-11 Adding ability to save config file

Этот коммит содержится в:
=Corey Hulen
2015-09-17 21:00:59 -07:00
родитель 82127341ca
Коммит 44714dfcb1
14 изменённых файлов: 843 добавлений и 296 удалений

Просмотреть файл

@@ -7,6 +7,7 @@ import (
"bufio"
"net/http"
"os"
"strings"
l4g "code.google.com/p/log4go"
"github.com/gorilla/mux"
@@ -20,6 +21,8 @@ func InitAdmin(r *mux.Router) {
sr := r.PathPrefix("/admin").Subrouter()
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
}
@@ -33,7 +36,7 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
if utils.Cfg.LogSettings.FileEnable {
file, err := os.Open(utils.Cfg.LogSettings.FileLocation)
file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
if err != nil {
c.Err = model.NewAppError("getLogs", "Error reading log file", err.Error())
}
@@ -54,3 +57,44 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(utils.ClientProperties)))
}
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getConfig") {
return
}
json := utils.Cfg.ToJson()
cfg := model.ConfigFromJson(strings.NewReader(json))
json = cfg.ToJson()
w.Write([]byte(json))
}
func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getConfig") {
return
}
cfg := model.ConfigFromJson(r.Body)
if cfg == nil {
c.SetInvalidParam("saveConfig", "config")
return
}
if len(cfg.ServiceSettings.Port) == 0 {
c.SetInvalidParam("saveConfig", "config")
return
}
if cfg.TeamSettings.MaxUsersPerTeam == 0 {
c.SetInvalidParam("saveConfig", "config")
return
}
// TODO run some cleanup validators
utils.SaveConfig(utils.CfgFileName, cfg)
utils.LoadConfig(utils.CfgFileName)
json := utils.Cfg.ToJson()
w.Write([]byte(json))
}

Просмотреть файл

@@ -8,6 +8,7 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
func TestGetLogs(t *testing.T) {
@@ -20,6 +21,12 @@ func TestGetLogs(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.GetLogs(); err == nil {
t.Fatal("Shouldn't have permissions")
}
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
@@ -37,8 +44,81 @@ func TestGetLogs(t *testing.T) {
func TestGetClientProperties(t *testing.T) {
Setup()
if _, err := Client.GetClientProperties(); err != nil {
if result, err := Client.GetClientProperties(); err != nil {
t.Fatal(err)
} else {
props := result.Data.(map[string]string)
if len(props["Version"]) == 0 {
t.Fatal()
}
}
}
func TestGetConfig(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.GetConfig(); err == nil {
t.Fatal("Shouldn't have permissions")
}
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
if result, err := Client.GetConfig(); err != nil {
t.Fatal(err)
} else {
cfg := result.Data.(*model.Config)
if len(cfg.ServiceSettings.SiteName) == 0 {
t.Fatal()
}
}
}
func TestSaveConfig(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.SaveConfig(utils.Cfg); err == nil {
t.Fatal("Shouldn't have permissions")
}
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
if result, err := Client.SaveConfig(utils.Cfg); err != nil {
t.Fatal(err)
} else {
cfg := result.Data.(*model.Config)
if len(cfg.ServiceSettings.SiteName) == 0 {
t.Fatal()
}
}
}

Просмотреть файл

@@ -9,13 +9,11 @@
},
"ServiceSettings": {
"SiteName": "Mattermost",
"Mode" : "dev",
"AllowTesting" : false,
"Mode": "dev",
"AllowTesting": false,
"UseSSL": false,
"Port": "8065",
"Version": "developer",
"Shards": {
},
"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
@@ -26,21 +24,12 @@
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false
},
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret" : "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
},
"SqlSettings": {
"DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
"DataSourceReplicas": ["mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"],
"DataSourceReplicas": [
"mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"
],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
@@ -62,7 +51,7 @@
"InitialFont": "luximbi.ttf"
},
"EmailSettings": {
"ByPassEmail" : true,
"ByPassEmail": true,
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
@@ -95,8 +84,20 @@
"MaxUsersPerTeam": 150,
"AllowPublicLink": true,
"AllowValetDefault": false,
"TourLink": "",
"DefaultThemeColor": "#2389D7",
"DisableTeamCreation": false,
"RestrictCreationToDomains": ""
},
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret": "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
}
}
}

Просмотреть файл

@@ -385,6 +385,24 @@ func (c *Client) GetClientProperties() (*Result, *AppError) {
}
}
func (c *Client) GetConfig() (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/config", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
}
}
func (c *Client) SaveConfig(config *Config) (*Result, *AppError) {
if r, err := c.DoApiPost("/admin/save_config", config.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
}
}
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err

151
model/config.go Обычный файл
Просмотреть файл

@@ -0,0 +1,151 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ServiceSettings struct {
SiteName string
Mode string
AllowTesting bool
UseSSL bool
Port string
Version string
InviteSalt string
PublicLinkSalt string
ResetSalt string
AnalyticsUrl string
UseLocalStorage bool
StorageDirectory string
AllowedLoginAttempts int
DisableEmailSignUp bool
EnableOAuthServiceProvider bool
}
type SSOSetting struct {
Allow bool
Secret string
Id string
Scope string
AuthEndpoint string
TokenEndpoint string
UserApiEndpoint string
}
type SqlSettings struct {
DriverName string
DataSource string
DataSourceReplicas []string
MaxIdleConns int
MaxOpenConns int
Trace bool
AtRestEncryptKey string
}
type LogSettings struct {
ConsoleEnable bool
ConsoleLevel string
FileEnable bool
FileLevel string
FileFormat string
FileLocation string
}
type AWSSettings struct {
S3AccessKeyId string
S3SecretAccessKey string
S3Bucket string
S3Region string
}
type ImageSettings struct {
ThumbnailWidth uint
ThumbnailHeight uint
PreviewWidth uint
PreviewHeight uint
ProfileWidth uint
ProfileHeight uint
InitialFont string
}
type EmailSettings struct {
ByPassEmail bool
SMTPUsername string
SMTPPassword string
SMTPServer string
UseTLS bool
UseStartTLS bool
FeedbackEmail string
FeedbackName string
ApplePushServer string
ApplePushCertPublic string
ApplePushCertPrivate string
}
type RateLimitSettings struct {
UseRateLimiter bool
PerSec int
MemoryStoreSize int
VaryByRemoteAddr bool
VaryByHeader string
}
type PrivacySettings struct {
ShowEmailAddress bool
ShowPhoneNumber bool
ShowSkypeId bool
ShowFullName bool
}
type ClientSettings struct {
SegmentDeveloperKey string
GoogleDeveloperKey string
}
type TeamSettings struct {
MaxUsersPerTeam int
AllowPublicLink bool
AllowValetDefault bool
TourLink string
DefaultThemeColor string
DisableTeamCreation bool
RestrictCreationToDomains string
}
type Config struct {
LogSettings LogSettings
ServiceSettings ServiceSettings
SqlSettings SqlSettings
AWSSettings AWSSettings
ImageSettings ImageSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
ClientSettings ClientSettings
TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
}
func (o *Config) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ConfigFromJson(data io.Reader) *Config {
decoder := json.NewDecoder(data)
var o Config
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

Просмотреть файл

@@ -6,11 +6,14 @@ package utils
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
l4g "code.google.com/p/log4go"
"github.com/mattermost/platform/model"
)
const (
@@ -20,139 +23,9 @@ const (
LOG_ROTATE_SIZE = 10000
)
type ServiceSettings struct {
SiteName string
Mode string
AllowTesting bool
UseSSL bool
Port string
Version string
InviteSalt string
PublicLinkSalt string
ResetSalt string
AnalyticsUrl string
UseLocalStorage bool
StorageDirectory string
AllowedLoginAttempts int
DisableEmailSignUp bool
EnableOAuthServiceProvider bool
}
type SSOSetting struct {
Allow bool
Secret string
Id string
Scope string
AuthEndpoint string
TokenEndpoint string
UserApiEndpoint string
}
type SqlSettings struct {
DriverName string
DataSource string
DataSourceReplicas []string
MaxIdleConns int
MaxOpenConns int
Trace bool
AtRestEncryptKey string
}
type LogSettings struct {
ConsoleEnable bool
ConsoleLevel string
FileEnable bool
FileLevel string
FileFormat string
FileLocation string
}
type AWSSettings struct {
S3AccessKeyId string
S3SecretAccessKey string
S3Bucket string
S3Region string
}
type ImageSettings struct {
ThumbnailWidth uint
ThumbnailHeight uint
PreviewWidth uint
PreviewHeight uint
ProfileWidth uint
ProfileHeight uint
InitialFont string
}
type EmailSettings struct {
ByPassEmail bool
SMTPUsername string
SMTPPassword string
SMTPServer string
UseTLS bool
UseStartTLS bool
FeedbackEmail string
FeedbackName string
ApplePushServer string
ApplePushCertPublic string
ApplePushCertPrivate string
}
type RateLimitSettings struct {
UseRateLimiter bool
PerSec int
MemoryStoreSize int
VaryByRemoteAddr bool
VaryByHeader string
}
type PrivacySettings struct {
ShowEmailAddress bool
ShowPhoneNumber bool
ShowSkypeId bool
ShowFullName bool
}
type ClientSettings struct {
SegmentDeveloperKey string
GoogleDeveloperKey string
}
type TeamSettings struct {
MaxUsersPerTeam int
AllowPublicLink bool
AllowValetDefault bool
TourLink string
DefaultThemeColor string
DisableTeamCreation bool
RestrictCreationToDomains string
}
type Config struct {
LogSettings LogSettings
ServiceSettings ServiceSettings
SqlSettings SqlSettings
AWSSettings AWSSettings
ImageSettings ImageSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
ClientSettings ClientSettings
TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
}
func (o *Config) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
var Cfg *Config = &Config{}
var Cfg *model.Config = &model.Config{}
var CfgLastModified int64 = 0
var CfgFileName string = ""
var ClientProperties map[string]string = map[string]string{}
var SanitizeOptions map[string]bool = map[string]bool{}
@@ -184,14 +57,14 @@ func FindDir(dir string) string {
}
func ConfigureCmdLineLog() {
ls := LogSettings{}
ls := model.LogSettings{}
ls.ConsoleEnable = true
ls.ConsoleLevel = "ERROR"
ls.FileEnable = false
configureLog(&ls)
}
func configureLog(s *LogSettings) {
func configureLog(s *model.LogSettings) {
l4g.Close()
@@ -207,12 +80,11 @@ func configureLog(s *LogSettings) {
}
if s.FileEnable {
if s.FileFormat == "" {
s.FileFormat = "[%D %T] [%L] %M"
}
if s.FileLocation == "" {
s.FileLocation = FindDir("logs") + "mattermost.log"
var fileFormat = s.FileFormat
if fileFormat == "" {
fileFormat = "[%D %T] [%L] %M"
}
level := l4g.DEBUG
@@ -222,14 +94,36 @@ func configureLog(s *LogSettings) {
level = l4g.ERROR
}
flw := l4g.NewFileLogWriter(s.FileLocation, false)
flw.SetFormat(s.FileFormat)
flw := l4g.NewFileLogWriter(GetLogFileLocation(s.FileLocation), false)
flw.SetFormat(fileFormat)
flw.SetRotate(true)
flw.SetRotateLines(LOG_ROTATE_SIZE)
l4g.AddFilter("file", level, flw)
}
}
func GetLogFileLocation(fileLocation string) string {
if fileLocation == "" {
return FindDir("logs") + "mattermost.log"
} else {
return fileLocation
}
}
func SaveConfig(fileName string, config *model.Config) *model.AppError {
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
}
err = ioutil.WriteFile(fileName, b, 0644)
if err != nil {
return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
}
return nil
}
// LoadConfig will try to search around for the corresponding config file.
// It will search /tmp/fileName then attempt ./config/fileName,
// then ../config/fileName and last it will look at fileName
@@ -243,7 +137,7 @@ func LoadConfig(fileName string) {
}
decoder := json.NewDecoder(file)
config := Config{}
config := model.Config{}
err = decoder.Decode(&config)
if err != nil {
panic("Error decoding config file=" + fileName + ", err=" + err.Error())
@@ -253,6 +147,7 @@ func LoadConfig(fileName string) {
panic("Error getting config info file=" + fileName + ", err=" + err.Error())
} else {
CfgLastModified = info.ModTime().Unix()
CfgFileName = fileName
}
configureLog(&config.LogSettings)
@@ -262,7 +157,7 @@ func LoadConfig(fileName string) {
ClientProperties = getClientProperties(Cfg)
}
func getSanitizeOptions(c *Config) map[string]bool {
func getSanitizeOptions(c *model.Config) map[string]bool {
options := map[string]bool{}
options["fullname"] = c.PrivacySettings.ShowFullName
options["email"] = c.PrivacySettings.ShowEmailAddress
@@ -272,7 +167,7 @@ func getSanitizeOptions(c *Config) map[string]bool {
return options
}
func getClientProperties(c *Config) map[string]string {
func getClientProperties(c *model.Config) map[string]string {
props := make(map[string]string)
props["Version"] = c.ServiceSettings.Version

Просмотреть файл

@@ -1,36 +1,63 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AdminSidebar = require('./admin_sidebar.jsx');
var EmailTab = require('./email_settings.jsx');
var JobsTab = require('./jobs_settings.jsx');
var LogsTab = require('./logs.jsx');
var Navbar = require('../../components/navbar.jsx');
var AdminSidebar = require('./admin_sidebar.jsx');
var AdminStore = require('../../stores/admin_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
var EmailSettingsTab = require('./email_settings.jsx');
var JobsSettingsTab = require('./jobs_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx');
var LogsTab = require('./logs.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
this.state = {
config: null,
selected: 'email_settings'
};
}
componentDidMount() {
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
AsyncClient.getConfig();
}
componentWillUnmount() {
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
}
onConfigListenerChange() {
this.setState({
config: AdminStore.getConfig(),
selected: this.state.selected
});
}
selectTab(tab) {
this.setState({selected: tab});
}
render() {
var tab = '';
var tab = <LoadingScreen />;
if (this.state.selected === 'email_settings') {
tab = <EmailTab />;
} else if (this.state.selected === 'job_settings') {
tab = <JobsTab />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
if (this.state.config != null) {
if (this.state.selected === 'email_settings') {
tab = <EmailSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'job_settings') {
tab = <JobsSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
} else if (this.state.selected === 'log_settings') {
tab = <LogSettingsTab config={this.state.config} />;
}
}
return (

Просмотреть файл

@@ -27,36 +27,36 @@ export default class AdminSidebar extends React.Component {
}
componentDidMount() {
$('.nav__menu-item').on('click', function clickme(e) {
e.preventDefault();
$(this).closest('.sidebar--collapsable').find('.nav__menu-item').removeClass('active');
$(this).addClass('active');
$(this).closest('.sidebar--collapsable').find('.nav__sub-menu').addClass('hide');
$(this).next('.nav__sub-menu').removeClass('hide');
});
// $('.nav__menu-item').on('click', function clickme(e) {
// e.preventDefault();
// $(this).closest('.sidebar--collapsable').find('.nav__menu-item').removeClass('active');
// $(this).addClass('active');
// $(this).closest('.sidebar--collapsable').find('.nav__sub-menu').addClass('hide');
// $(this).next('.nav__sub-menu').removeClass('hide');
// });
$('.nav__sub-menu a').on('click', function clickme(e) {
e.preventDefault();
$(this).closest('.nav__sub-menu').find('a').removeClass('active');
$(this).addClass('active');
});
// $('.nav__sub-menu a').on('click', function clickme(e) {
// e.preventDefault();
// $(this).closest('.nav__sub-menu').find('a').removeClass('active');
// $(this).addClass('active');
// });
$('.nav__sub-menu-item').on('click', function clickme(e) {
e.preventDefault();
$(this).closest('.sidebar--collapsable').find('.nav__inner-menu').addClass('hide');
$(this).closest('li').next('li').find('.nav__inner-menu').removeClass('hide');
$(this).closest('li').next('li').find('.nav__inner-menu li:first a').addClass('active');
});
// $('.nav__sub-menu-item').on('click', function clickme(e) {
// e.preventDefault();
// $(this).closest('.sidebar--collapsable').find('.nav__inner-menu').addClass('hide');
// $(this).closest('li').next('li').find('.nav__inner-menu').removeClass('hide');
// $(this).closest('li').next('li').find('.nav__inner-menu li:first a').addClass('active');
// });
$('.nav__inner-menu a').on('click', function clickme() {
$(this).closest('.nav__inner-menu').closest('li').prev('li').find('a').addClass('active');
});
// $('.nav__inner-menu a').on('click', function clickme() {
// $(this).closest('.nav__inner-menu').closest('li').prev('li').find('a').addClass('active');
// });
$('.nav__sub-menu .menu__close').on('click', function close() {
var menuItem = $(this).closest('li');
menuItem.next('li').remove();
menuItem.remove();
});
// $('.nav__sub-menu .menu__close').on('click', function close() {
// var menuItem = $(this).closest('li');
// menuItem.next('li').remove();
// menuItem.remove();
// });
}
render() {
@@ -69,10 +69,6 @@ export default class AdminSidebar extends React.Component {
/>
<ul className='nav nav-pills nav-stacked'>
<li>
<a href='#'
className='nav__menu-item active'
>
<span className='icon fa fa-gear'></span> <span>{'Basic Settings'}</span></a>
<ul className='nav nav__sub-menu'>
<li>
<a
@@ -83,6 +79,15 @@ export default class AdminSidebar extends React.Component {
{'Email Settings'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('log_settings')}
onClick={this.handleClick.bind(null, 'log_settings')}
>
{'Log Settings'}
</a>
</li>
<li>
<a
href='#'
@@ -92,16 +97,6 @@ export default class AdminSidebar extends React.Component {
{'Logs'}
</a>
</li>
</ul>
</li>
<li>
<a
href='#'
className='nav__menu-item'
>
<span className='icon fa fa-gear'></span> <span>{'Jobs'}</span>
</a>
<ul className='nav nav__sub-menu hide'>
<li>
<a
href='#'
@@ -113,85 +108,6 @@ export default class AdminSidebar extends React.Component {
</li>
</ul>
</li>
<li>
<a
href='#'
className='nav__menu-item'
>
<span className='icon fa fa-gear'></span>
<span>{'Team Settings (306)'}</span>
<span className='menu-icon--right'>
<i className='fa fa-plus'></i>
</span>
</a>
<ul className='nav nav__sub-menu hide'>
<li>
<a
href='#'
className='nav__sub-menu-item active'
>
{'Adal '}
<span className='menu-icon--right menu__close'>{'x'}</span>
</a>
</li>
<li>
<ul className='nav nav__inner-menu'>
<li>
<a
href='#'
className='active'
>
{'- Users'}
</a>
</li>
<li><a href='#'>{'- View Statistics'}</a></li>
<li>
<a href='#'>
{'- View Audit Log'}
<span className='badge pull-right small'>{'1'}</span>
</a>
</li>
</ul>
</li>
<li>
<a
href='#'
className='nav__sub-menu-item'
>
{'Boole '}
<span className='menu-icon--right menu__close'>{'x'}</span>
</a>
</li>
<li>
<ul className='nav nav__inner-menu hide'>
<li>
<a
href='#'
className='active'
>
{'- Users'}
</a>
</li>
<li><a href='#'>{'- View Statistics'}</a></li>
<li>
<a href='#'>
{'- View Audit Log'}
<span className='badge pull-right small'>{'1'}</span>
</a>
</li>
</ul>
</li>
<li>
<span
data-toggle='modal'
data-target='#select-team'
className='nav-more'
>
{'Select a team'}
</span>
</li>
</ul>
</li>
</ul>
</div>
</div>

Просмотреть файл

@@ -0,0 +1,67 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var NavbarDropdown = require('./navbar_dropdown.jsx');
var UserStore = require('../stores/user_store.jsx');
export default class SidebarHeader extends React.Component {
constructor(props) {
super(props);
this.toggleDropdown = this.toggleDropdown.bind(this);
this.state = {};
}
toggleDropdown() {
if (this.refs.dropdown.blockToggle) {
this.refs.dropdown.blockToggle = false;
return;
}
$('.team__header').find('.dropdown-toggle').dropdown('toggle');
}
render() {
var me = UserStore.getCurrentUser();
var profilePicture = null;
if (!me) {
return null;
}
if (me.last_picture_update) {
profilePicture = (
<img
className='user__picture'
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
/>
);
}
return (
<div className='team__header theme'>
<a
href='#'
onClick={this.toggleDropdown}
>
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
<div className='team__name'>{this.props.teamDisplayName}</div>
</div>
</a>
<NavbarDropdown
ref='dropdown'
teamType={this.props.teamType}
/>
</div>
);
}
}
SidebarHeader.defaultProps = {
teamDisplayName: global.window.config.SiteName,
teamType: ''
};
SidebarHeader.propTypes = {
teamDisplayName: React.PropTypes.string,
teamType: React.PropTypes.string
};

Просмотреть файл

@@ -0,0 +1,261 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class LogSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
saveNeeded: false,
serverError: null
};
}
handleChange() {
this.setState({saveNeeded: true, serverError: this.state.serverError});
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.LogSettings.ConsoleEnable = React.findDOMNode(this.refs.consoleEnable).checked;
config.LogSettings.ConsoleLevel = React.findDOMNode(this.refs.consoleLevel).value;
config.LogSettings.FileEnable = React.findDOMNode(this.refs.fileEnable).checked;
config.LogSettings.FileLevel = React.findDOMNode(this.refs.fileLevel).value;
config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim();
config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim();
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({serverError: null, saveNeeded: false});
$('#save-button').button('reset');
},
(err) => {
this.setState({serverError: err.message, saveNeeded: true});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
return (
<div className='wrapper--fixed'>
<h3>{'Log Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='consoleEnable'
>
{'Log To the Console: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='consoleEnable'
value='true'
ref='consoleEnable'
defaultChecked={this.props.config.LogSettings.ConsoleEnable}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='consoleEnable'
value='false'
defaultChecked={!this.props.config.LogSettings.ConsoleEnable}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true then the server will output messages to the standard output stream (stdout).'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='consoleLevel'
>
{'Console Log Level:'}
</label>
<div className='col-sm-8'>
<select
className='form-control'
id='consoleLevel'
ref='consoleLevel'
defaultValue={this.props.config.LogSettings.consoleLevel}
onChange={this.handleChange}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
<p className='help-text'>{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
{'Log To File: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='fileEnable'
ref='fileEnable'
value='true'
defaultChecked={this.props.config.LogSettings.FileEnable}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='fileEnable'
value='false'
defaultChecked={!this.props.config.LogSettings.FileEnable}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Typically set to true in production. When true log files are written to the file specified in file location field below.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='fileLevel'
>
{'File Log Level:'}
</label>
<div className='col-sm-8'>
<select
className='form-control'
id='fileLevel'
ref='fileLevel'
defaultValue={this.props.config.LogSettings.FileLevel}
onChange={this.handleChange}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
<p className='help-text'>{'This setting determines the level of detail at which log events are written to the file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='fileLocation'
>
{'File Location:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='fileLocation'
ref='fileLocation'
placeholder='Enter your file location'
defaultValue={this.props.config.LogSettings.FileLocation}
onChange={this.handleChange}
/>
<p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='fileFormat'
>
{'File Format:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='fileFormat'
ref='fileFormat'
placeholder='Enter your file format'
defaultValue={this.props.config.LogSettings.FileFormat}
onChange={this.handleChange}
/>
<p className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
<div className='help-text'>
<table
className='table-bordered'
cellPadding='5'
>
<tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
<tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
<tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
<tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
<tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
<tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
</table>
</div>
</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
LogSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -21,9 +21,11 @@ export default class Logs extends React.Component {
AdminStore.addLogChangeListener(this.onLogListenerChange);
AsyncClient.getLogs();
}
componentWillUnmount() {
AdminStore.removeLogChangeListener(this.onLogListenerChange);
}
onLogListenerChange() {
this.setState({
logs: AdminStore.getLogs()

Просмотреть файл

@@ -8,16 +8,22 @@ var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var LOG_CHANGE_EVENT = 'log_change';
var CONFIG_CHANGE_EVENT = 'config_change';
class AdminStoreClass extends EventEmitter {
constructor() {
super();
this.logs = null;
this.config = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
}
emitLogChange() {
@@ -32,6 +38,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(LOG_CHANGE_EVENT, callback);
}
emitConfigChange() {
this.emit(CONFIG_CHANGE_EVENT);
}
addConfigChangeListener(callback) {
this.on(CONFIG_CHANGE_EVENT, callback);
}
removeConfigChangeListener(callback) {
this.removeListener(CONFIG_CHANGE_EVENT, callback);
}
getLogs() {
return this.logs;
}
@@ -39,6 +57,14 @@ class AdminStoreClass extends EventEmitter {
saveLogs(logs) {
this.logs = logs;
}
getConfig() {
return this.config;
}
saveConfig(config) {
this.config = config;
}
}
var AdminStore = new AdminStoreClass();
@@ -51,6 +77,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveLogs(action.logs);
AdminStore.emitLogChange();
break;
case ActionTypes.RECIEVED_CONFIG:
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();
break;
default:
}
});

Просмотреть файл

@@ -345,6 +345,32 @@ export function getLogs() {
);
}
export function getConfig() {
if (isCallInProgress('getConfig')) {
return;
}
callTracker.getConfig = utils.getTimestamp();
client.getConfig(
(data, textStatus, xhr) => {
callTracker.getConfig = 0;
if (xhr.status === 304 || !data) {
return;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECIEVED_CONFIG,
config: data
});
},
(err) => {
callTracker.getConfig = 0;
dispatchError(err, 'getConfig');
}
);
}
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;

Просмотреть файл

@@ -297,7 +297,7 @@ export function getLogs(success, error) {
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
success,
error: function onError(xhr, status, err) {
var e = handleError('getLogs', xhr, status, err);
error(e);
@@ -305,6 +305,35 @@ export function getLogs(success, error) {
});
}
export function getConfig(success, error) {
$.ajax({
url: '/api/v1/admin/config',
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success,
error: function onError(xhr, status, err) {
var e = handleError('getConfig', xhr, status, err);
error(e);
}
});
}
export function saveConfig(config, success, error) {
$.ajax({
url: '/api/v1/admin/save_config',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(config),
success,
error: function onError(xhr, status, err) {
var e = handleError('saveConfig', xhr, status, err);
error(e);
}
});
}
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({