summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristopher Speller <crspeller@gmail.com>2015-08-26 12:49:07 -0400
committerChristopher Speller <crspeller@gmail.com>2015-09-04 11:11:38 -0400
commitf0fd9a9e8b85544089246bde71b6d61ba90eeb0e (patch)
tree256896117078ef1396a52fcecf3116863b3bb716
parent1b923528448eace438a1f498116a19361a8b0fb2 (diff)
downloadchat-f0fd9a9e8b85544089246bde71b6d61ba90eeb0e.tar.gz
chat-f0fd9a9e8b85544089246bde71b6d61ba90eeb0e.tar.bz2
chat-f0fd9a9e8b85544089246bde71b6d61ba90eeb0e.zip
Adding ability to export data from mattermost
-rw-r--r--api/export.go292
-rw-r--r--api/file.go42
-rw-r--r--api/team.go21
-rw-r--r--model/channel.go3
-rw-r--r--model/post.go3
-rw-r--r--model/team.go3
-rw-r--r--model/user.go10
-rw-r--r--store/sql_channel_store.go22
-rw-r--r--store/sql_post_store.go24
-rw-r--r--store/sql_team_store.go20
-rw-r--r--store/sql_user_store.go27
-rw-r--r--store/store.go4
-rw-r--r--web/react/components/team_export_tab.jsx96
-rw-r--r--web/react/components/team_settings.jsx8
-rw-r--r--web/react/components/team_settings_modal.jsx1
-rw-r--r--web/react/utils/client.jsx13
16 files changed, 589 insertions, 0 deletions
diff --git a/api/export.go b/api/export.go
new file mode 100644
index 000000000..9345f892f
--- /dev/null
+++ b/api/export.go
@@ -0,0 +1,292 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ "archive/zip"
+ "encoding/json"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "io"
+ "os"
+)
+
+const (
+ EXPORT_PATH = "export/"
+ EXPORT_FILENAME = "MattermostExport.zip"
+ EXPORT_OPTIONS_FILE = "options.json"
+ EXPORT_TEAMS_FOLDER = "teams"
+ EXPORT_CHANNELS_FOLDER = "channels"
+ EXPORT_CHANNEL_MEMBERS_FOLDER = "members"
+ EXPORT_POSTS_FOLDER = "posts"
+ EXPORT_USERS_FOLDER = "users"
+ EXPORT_LOCAL_STORAGE_FOLDER = "files"
+)
+
+type ExportWriter interface {
+ Create(name string) (io.Writer, error)
+}
+
+type ExportOptions struct {
+ TeamsToExport []string `json:"teams"`
+ ChannelsToExport []string `json:"channels"`
+ UsersToExport []string `json:"users"`
+ ExportLocalStorage bool `json:"export_local_storage"`
+}
+
+func (options *ExportOptions) ToJson() string {
+ b, err := json.Marshal(options)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func ExportOptionsFromJson(data io.Reader) *ExportOptions {
+ decoder := json.NewDecoder(data)
+ var o ExportOptions
+ decoder.Decode(&o)
+ return &o
+}
+
+func ExportToFile(options *ExportOptions) (link string, err *model.AppError) {
+ // Open file for export
+ if file, err := openFileWriteStream(EXPORT_PATH + EXPORT_FILENAME); err != nil {
+ return "", err
+ } else {
+ defer closeFileWriteStream(file)
+ ExportToWriter(file, options)
+ }
+
+ return "/api/v1/files/get_export", nil
+}
+
+func ExportToWriter(w io.Writer, options *ExportOptions) *model.AppError {
+ // Open a writer to write to zip file
+ zipWriter := zip.NewWriter(w)
+ defer zipWriter.Close()
+
+ // Write our options to file
+ if optionsFile, err := zipWriter.Create(EXPORT_OPTIONS_FILE); err != nil {
+ return model.NewAppError("ExportToWriter", "Unable to create options file", err.Error())
+ } else {
+ if _, err := optionsFile.Write([]byte(options.ToJson())); err != nil {
+ return model.NewAppError("ExportToWriter", "Unable to write to options file", err.Error())
+ }
+ }
+
+ // Export Teams
+ ExportTeams(zipWriter, options)
+
+ return nil
+}
+
+func ExportTeams(writer ExportWriter, options *ExportOptions) *model.AppError {
+ // Get the teams
+ var teams []*model.Team
+ if len(options.TeamsToExport) == 0 {
+ if result := <-Srv.Store.Team().GetForExport(); result.Err != nil {
+ return result.Err
+ } else {
+ teams = result.Data.([]*model.Team)
+ }
+ } else {
+ for _, teamId := range options.TeamsToExport {
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ return result.Err
+ } else {
+ team := result.Data.(*model.Team)
+ teams = append(teams, team)
+ }
+ }
+ }
+
+ // Export the teams
+ for i := range teams {
+ // Sanitize
+ teams[i].PreExport()
+
+ if teamFile, err := writer.Create(EXPORT_TEAMS_FOLDER + "/" + teams[i].Name + ".json"); err != nil {
+ return model.NewAppError("ExportTeams", "Unable to open file for export", err.Error())
+ } else {
+ if _, err := teamFile.Write([]byte(teams[i].ToJson())); err != nil {
+ return model.NewAppError("ExportTeams", "Unable to write to team export file", err.Error())
+ }
+ }
+
+ }
+
+ // Export the channels, local storage and users
+ for _, team := range teams {
+ if err := ExportChannels(writer, options, team.Id); err != nil {
+ return err
+ }
+ if err := ExportUsers(writer, options, team.Id); err != nil {
+ return err
+ }
+ if err := ExportLocalStorage(writer, options, team.Id); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func ExportChannels(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
+ // Get the channels
+ var channels []*model.Channel
+ if len(options.ChannelsToExport) == 0 {
+ if result := <-Srv.Store.Channel().GetForExport(teamId); result.Err != nil {
+ return result.Err
+ } else {
+ channels = result.Data.([]*model.Channel)
+ }
+ } else {
+ for _, channelId := range options.ChannelsToExport {
+ if result := <-Srv.Store.Channel().Get(channelId); result.Err != nil {
+ return result.Err
+ } else {
+ channel := result.Data.(*model.Channel)
+ channels = append(channels, channel)
+ }
+ }
+ }
+
+ for i := range channels {
+ // Get members
+ mchan := Srv.Store.Channel().GetMembers(channels[i].Id)
+
+ // Sanitize
+ channels[i].PreExport()
+
+ if channelFile, err := writer.Create(EXPORT_CHANNELS_FOLDER + "/" + channels[i].Id + ".json"); err != nil {
+ return model.NewAppError("ExportChannels", "Unable to open file for export", err.Error())
+ } else {
+ if _, err := channelFile.Write([]byte(channels[i].ToJson())); err != nil {
+ return model.NewAppError("ExportChannels", "Unable to write to export file", err.Error())
+ }
+ }
+
+ var members []model.ChannelMember
+ if result := <-mchan; result.Err != nil {
+ return result.Err
+ } else {
+ members = result.Data.([]model.ChannelMember)
+ }
+
+ if membersFile, err := writer.Create(EXPORT_CHANNELS_FOLDER + "/" + channels[i].Id + "_members.json"); err != nil {
+ return model.NewAppError("ExportChannels", "Unable to open file for export", err.Error())
+ } else {
+ result, err2 := json.Marshal(members)
+ if err2 != nil {
+ return model.NewAppError("ExportChannels", "Unable to convert to json", err.Error())
+ }
+ if _, err3 := membersFile.Write([]byte(result)); err3 != nil {
+ return model.NewAppError("ExportChannels", "Unable to write to export file", err.Error())
+ }
+ }
+ }
+
+ for _, channel := range channels {
+ if err := ExportPosts(writer, options, channel.Id); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func ExportPosts(writer ExportWriter, options *ExportOptions, channelId string) *model.AppError {
+ // Get the posts
+ var posts []*model.Post
+ if result := <-Srv.Store.Post().GetForExport(channelId); result.Err != nil {
+ return result.Err
+ } else {
+ posts = result.Data.([]*model.Post)
+ }
+
+ // Export the posts
+ if postsFile, err := writer.Create(EXPORT_POSTS_FOLDER + "/" + channelId + "_posts.json"); err != nil {
+ return model.NewAppError("ExportPosts", "Unable to open file for export", err.Error())
+ } else {
+ result, err2 := json.Marshal(posts)
+ if err2 != nil {
+ return model.NewAppError("ExportPosts", "Unable to convert to json", err.Error())
+ }
+ if _, err3 := postsFile.Write([]byte(result)); err3 != nil {
+ return model.NewAppError("ExportPosts", "Unable to write to export file", err.Error())
+ }
+ }
+
+ return nil
+}
+
+func ExportUsers(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
+ // Get the users
+ var users []*model.User
+ if result := <-Srv.Store.User().GetForExport(teamId); result.Err != nil {
+ return result.Err
+ } else {
+ users = result.Data.([]*model.User)
+ }
+
+ // Write the users
+ if usersFile, err := writer.Create(EXPORT_USERS_FOLDER + "/" + teamId + "_users.json"); err != nil {
+ return model.NewAppError("ExportUsers", "Unable to open file for export", err.Error())
+ } else {
+ result, err2 := json.Marshal(users)
+ if err2 != nil {
+ return model.NewAppError("ExportUsers", "Unable to convert to json", err.Error())
+ }
+ if _, err3 := usersFile.Write([]byte(result)); err3 != nil {
+ return model.NewAppError("ExportUsers", "Unable to write to export file", err.Error())
+ }
+ }
+ return nil
+}
+
+func copyDirToExportWriter(writer ExportWriter, inPath string, outPath string) *model.AppError {
+ dir, err := os.Open(inPath)
+ if err != nil {
+ return model.NewAppError("copyDirToExportWriter", "Unable to open directory", err.Error())
+ }
+
+ fileInfoList, err := dir.Readdir(0)
+ if err != nil {
+ return model.NewAppError("copyDirToExportWriter", "Unable to read directory", err.Error())
+ }
+
+ for _, fileInfo := range fileInfoList {
+ if fileInfo.IsDir() {
+ copyDirToExportWriter(writer, inPath+"/"+fileInfo.Name(), outPath+"/"+fileInfo.Name())
+ } else {
+ if toFile, err := writer.Create(outPath + "/" + fileInfo.Name()); err != nil {
+ return model.NewAppError("copyDirToExportWriter", "Unable to open file for export", err.Error())
+ } else {
+ fromFile, err := os.Open(inPath + "/" + fileInfo.Name())
+ if err != nil {
+ return model.NewAppError("copyDirToExportWriter", "Unable to open file", err.Error())
+ }
+ io.Copy(toFile, fromFile)
+ }
+ }
+ }
+
+ return nil
+}
+
+func ExportLocalStorage(writer ExportWriter, options *ExportOptions, teamId string) *model.AppError {
+ teamDir := utils.Cfg.ServiceSettings.StorageDirectory + "teams/" + teamId
+
+ if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ return model.NewAppError("ExportLocalStorage", "S3 is not supported for local storage export.", "")
+ } else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
+ if err := copyDirToExportWriter(writer, teamDir, EXPORT_LOCAL_STORAGE_FOLDER); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/api/file.go b/api/file.go
index 800c512c5..1d8244fac 100644
--- a/api/file.go
+++ b/api/file.go
@@ -40,6 +40,7 @@ func InitFile(r *mux.Router) {
sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET")
sr.Handle("/get_info/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFileInfo)).Methods("GET")
sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST")
+ sr.Handle("/get_export", ApiUserRequired(getExport)).Methods("GET")
}
func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -414,6 +415,23 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(rData)))
}
+func getExport(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasPermissionsToTeam(c.Session.TeamId, "export") || !c.IsTeamAdmin(c.Session.UserId) {
+ c.Err = model.NewAppError("getExport", "Only a team admin can retrieve exported data.", "userId="+c.Session.UserId)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ data, err := readFile(EXPORT_PATH + EXPORT_FILENAME)
+ if err != nil {
+ c.Err = model.NewAppError("getExport", "Unable to retrieve exported file. Please re-export", err.Error())
+ return
+ }
+
+ w.Header().Set("Content-Disposition", "attachment; filename="+EXPORT_FILENAME)
+ w.Header().Set("Content-Type", "application/octet-stream")
+ w.Write(data)
+}
+
func writeFile(f []byte, path string) *model.AppError {
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
@@ -488,3 +506,27 @@ func readFile(path string) ([]byte, *model.AppError) {
return nil, model.NewAppError("readFile", "File storage not configured properly. Please configure for either S3 or local server file storage.", "")
}
}
+
+func openFileWriteStream(path string) (io.Writer, *model.AppError) {
+ if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
+ return nil, model.NewAppError("openFileWriteStream", "S3 is not supported.", "")
+ } else if utils.Cfg.ServiceSettings.UseLocalStorage && len(utils.Cfg.ServiceSettings.StorageDirectory) > 0 {
+ if err := os.MkdirAll(filepath.Dir(utils.Cfg.ServiceSettings.StorageDirectory+path), 0774); err != nil {
+ return nil, model.NewAppError("openFileWriteStream", "Encountered an error creating the directory for the new file", err.Error())
+ }
+
+ if fileHandle, err := os.Create(utils.Cfg.ServiceSettings.StorageDirectory + path); err != nil {
+ return nil, model.NewAppError("openFileWriteStream", "Encountered an error writing to local server storage", err.Error())
+ } else {
+ fileHandle.Chmod(0644)
+ return fileHandle, nil
+ }
+
+ }
+
+ return nil, model.NewAppError("openFileWriteStream", "File storage not configured properly. Please configure for either S3 or local server file storage.", "")
+}
+
+func closeFileWriteStream(file io.Writer) {
+ file.(*os.File).Close()
+}
diff --git a/api/team.go b/api/team.go
index 8cce384c3..e1b3b274a 100644
--- a/api/team.go
+++ b/api/team.go
@@ -32,7 +32,9 @@ func InitTeam(r *mux.Router) {
sr.Handle("/update_name", ApiUserRequired(updateTeamDisplayName)).Methods("POST")
sr.Handle("/update_valet_feature", ApiUserRequired(updateValetFeature)).Methods("POST")
sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
+ // These should be moved to the global admain console
sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST")
+ sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET")
}
func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -675,3 +677,22 @@ func importTeam(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeContent(w, r, "MattermostImportLog.txt", time.Now(), bytes.NewReader(log.Bytes()))
}
+
+func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasPermissionsToTeam(c.Session.TeamId, "export") || !c.IsTeamAdmin(c.Session.UserId) {
+ c.Err = model.NewAppError("exportTeam", "Only a team admin can export data.", "userId="+c.Session.UserId)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ options := ExportOptionsFromJson(r.Body)
+
+ if link, err := ExportToFile(options); err != nil {
+ c.Err = err
+ return
+ } else {
+ result := map[string]string{}
+ result["link"] = link
+ w.Write([]byte(model.MapToJson(result)))
+ }
+}
diff --git a/model/channel.go b/model/channel.go
index b46f79f75..7d8edeee7 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -117,3 +117,6 @@ func (o *Channel) PreUpdate() {
func (o *Channel) ExtraUpdated() {
o.ExtraUpdateAt = GetMillis()
}
+
+func (o *Channel) PreExport() {
+}
diff --git a/model/post.go b/model/post.go
index 0c035d4e7..e78469940 100644
--- a/model/post.go
+++ b/model/post.go
@@ -147,3 +147,6 @@ func (o *Post) AddProp(key string, value string) {
o.Props[key] = value
}
+
+func (o *Post) PreExport() {
+}
diff --git a/model/team.go b/model/team.go
index 95e2757c8..6006f738c 100644
--- a/model/team.go
+++ b/model/team.go
@@ -197,3 +197,6 @@ func CleanTeamName(s string) string {
return s
}
+
+func (o *Team) PreExport() {
+}
diff --git a/model/user.go b/model/user.go
index d82f96db3..9f90b8204 100644
--- a/model/user.go
+++ b/model/user.go
@@ -272,6 +272,16 @@ func (u *User) GetDisplayName() string {
}
}
+func (u *User) PreExport() {
+ u.Password = ""
+ u.AuthData = ""
+ u.LastActivityAt = 0
+ u.LastPingAt = 0
+ u.LastPasswordUpdate = 0
+ u.LastPictureUpdate = 0
+ u.FailedAttempts = 0
+}
+
// UserFromJson will decode the input and return a User
func UserFromJson(data io.Reader) *User {
decoder := json.NewDecoder(data)
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index d2e3943df..b58166fd6 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -678,3 +678,25 @@ func (s SqlChannelStore) UpdateNotifyLevel(channelId, userId, notifyLevel string
return storeChannel
}
+
+func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var data []*model.Channel
+ _, err := s.GetReplica().Select(&data, "SELECT * FROM Channels WHERE TeamId = :TeamId AND DeleteAt = 0 AND Type = 'O'", map[string]interface{}{"TeamId": teamId})
+
+ if err != nil {
+ result.Err = model.NewAppError("SqlChannelStore.GetAllChannels", "We couldn't get all the channels", "teamId="+teamId+", err="+err.Error())
+ } else {
+ result.Data = data
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index 297d60397..20de23eb7 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -506,3 +506,27 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
return storeChannel
}
+
+func (s SqlPostStore) GetForExport(channelId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var posts []*model.Post
+ _, err := s.GetReplica().Select(
+ &posts,
+ "SELECT * FROM Posts WHERE ChannelId = :ChannelId AND DeleteAt = 0",
+ map[string]interface{}{"ChannelId": channelId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlPostStore.GetForExport", "We couldn't get the posts for the channel", "channelId="+channelId+err.Error())
+ } else {
+ result.Data = posts
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 2784f8630..fcbcaab9f 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -193,3 +193,23 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
return storeChannel
}
+
+func (s SqlTeamStore) GetForExport() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var data []*model.Team
+ if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil {
+ result.Err = model.NewAppError("SqlTeamStore.GetForExport", "We could not get all teams", err.Error())
+ }
+
+ result.Data = data
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 64a18545a..be1d29df0 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -452,3 +452,30 @@ func (us SqlUserStore) VerifyEmail(userId string) StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var users []*model.User
+
+ if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetProfiles", "We encounted an error while finding user profiles", err.Error())
+ } else {
+ for _, u := range users {
+ u.Password = ""
+ u.AuthData = ""
+ }
+
+ result.Data = users
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/store.go b/store/store.go
index 271caa366..959e93fa4 100644
--- a/store/store.go
+++ b/store/store.go
@@ -44,6 +44,7 @@ type TeamStore interface {
Get(id string) StoreChannel
GetByName(name string) StoreChannel
GetTeamsForEmail(domain string) StoreChannel
+ GetForExport() StoreChannel
}
type ChannelStore interface {
@@ -55,6 +56,7 @@ type ChannelStore interface {
GetChannels(teamId string, userId string) StoreChannel
GetMoreChannels(teamId string, userId string) StoreChannel
GetChannelCounts(teamId string, userId string) StoreChannel
+ GetForExport(teamId string) StoreChannel
SaveMember(member *model.ChannelMember) StoreChannel
GetMembers(channelId string) StoreChannel
@@ -78,6 +80,7 @@ type PostStore interface {
GetPostsSince(channelId string, time int64) StoreChannel
GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
+ GetForExport(channelId string) StoreChannel
}
type UserStore interface {
@@ -96,6 +99,7 @@ type UserStore interface {
VerifyEmail(userId string) StoreChannel
GetEtagForProfiles(teamId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
+ GetForExport(teamId string) StoreChannel
}
type SessionStore interface {
diff --git a/web/react/components/team_export_tab.jsx b/web/react/components/team_export_tab.jsx
new file mode 100644
index 000000000..1bc5abdb1
--- /dev/null
+++ b/web/react/components/team_export_tab.jsx
@@ -0,0 +1,96 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../utils/client.jsx');
+
+export default class TeamExportTab extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {status: 'request', link: '', err: ''};
+
+ this.onExportSuccess = this.onExportSuccess.bind(this);
+ this.onExportFailure = this.onExportFailure.bind(this);
+ this.doExport = this.doExport.bind(this);
+ }
+ onExportSuccess(data) {
+ this.setState({status: 'ready', link: data.link, err: ''});
+ }
+ onExportFailure(e) {
+ this.setState({status: 'failure', link: '', err: e.message});
+ }
+ doExport() {
+ if (this.state.status === 'in-progress') {
+ return;
+ }
+ this.setState({status: 'in-progress'});
+ Client.exportTeam(this.onExportSuccess, this.onExportFailure);
+ }
+ render() {
+ var messageSection = '';
+ switch (this.state.status) {
+ case 'request':
+ messageSection = '';
+ break;
+ case 'in-progress':
+ messageSection = (
+ <p className='confirm-import alert alert-warning'>
+ <i className='fa fa-spinner fa-pulse' />
+ {' Exporting...'}
+ </p>
+ );
+ break;
+ case 'ready':
+ messageSection = (
+ <p className='confirm-import alert alert-success'>
+ <i className='fa fa-check' />
+ {' Ready for '}
+ <a
+ href={this.state.link}
+ download={true}
+ >
+ {'download'}
+ </a>
+ </p>
+ );
+ break;
+ case 'failure':
+ messageSection = (
+ <p className='confirm-import alert alert-warning'>
+ <i className='fa fa-warning' />
+ {' Unable to export: ' + this.state.err}
+ </p>
+ );
+ break;
+ }
+
+ return (
+ <div
+ ref='wrapper'
+ className='user-settings'
+ >
+ <h3 className='tab-header'>{'Export'}</h3>
+ <div className='divider-dark first'/>
+ <ul className='section-max'>
+ <li className='col-xs-12 section-title'>{'Export your team'}</li>
+ <li className='col-xs-offset-3 col-xs-8'>
+ <ul className='setting-list'>
+ <li className='setting-list-item'>
+ <span className='btn btn-sm btn-primary btn-file sel-btn'>
+ <a
+ className='btn btn-sm btn-primary'
+ href='#'
+ onClick={this.doExport}
+ >
+ {'Export'}
+ </a>
+ </span>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <div className='divider-dark'/>
+ {messageSection}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index 53855fe1c..396521af9 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -3,6 +3,7 @@
var TeamStore = require('../stores/team_store.jsx');
var ImportTab = require('./team_import_tab.jsx');
+var ExportTab = require('./team_export_tab.jsx');
var FeatureTab = require('./team_feature_tab.jsx');
var GeneralTab = require('./team_general_tab.jsx');
var Utils = require('../utils/utils.jsx');
@@ -64,6 +65,13 @@ export default class TeamSettings extends React.Component {
</div>
);
break;
+ case 'export':
+ result = (
+ <div>
+ <ExportTab />
+ </div>
+ );
+ break;
default:
result = (
<div/>
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 668bf76cf..0513c811f 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -36,6 +36,7 @@ export default class TeamSettingsModal extends React.Component {
let tabs = [];
tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'});
tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'});
+ tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'});
return (
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 10f9c0b37..51fd16474 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -919,6 +919,19 @@ export function importSlack(fileData, success, error) {
});
}
+export function exportTeam(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/export_team',
+ type: 'GET',
+ dataType: 'json',
+ success: success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('exportTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getStatuses(success, error) {
$.ajax({
url: '/api/v1/users/status',