Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
2493bb0f
Unverified
Commit
2493bb0f
authored
Feb 14, 2023
by
Zeng1998
Committed by
GitHub
Feb 14, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: storage service frontend (#1088)
parent
4641e89c
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
645 additions
and
18 deletions
+645
-18
extensions.json
.vscode/extensions.json
+1
-3
project.code-workspace
.vscode/project.code-workspace
+2
-2
system.go
api/system.go
+1
-0
system_setting.go
api/system_setting.go
+5
-5
resource.go
server/resource.go
+21
-3
system.go
server/system.go
+4
-1
storage.go
store/storage.go
+3
-0
CreateStorageServiceDialog.tsx
web/src/components/CreateStorageServiceDialog.tsx
+169
-0
SettingDialog.tsx
web/src/components/SettingDialog.tsx
+10
-1
StorageSection.tsx
web/src/components/Settings/StorageSection.tsx
+65
-0
UpdateStorageServiceDialog.tsx
web/src/components/UpdateStorageServiceDialog.tsx
+189
-0
api.ts
web/src/helpers/api.ts
+16
-0
storage-section.less
web/src/less/settings/storage-section.less
+14
-0
en.json
web/src/locales/en.json
+10
-1
zh.json
web/src/locales/zh.json
+11
-2
index.ts
web/src/store/index.ts
+2
-0
index.ts
web/src/store/module/index.ts
+1
-0
storage.ts
web/src/store/module/storage.ts
+31
-0
storage.ts
web/src/store/reducer/storage.ts
+53
-0
storage.d.ts
web/src/types/modules/storage.d.ts
+36
-0
system.d.ts
web/src/types/modules/system.d.ts
+1
-0
No files found.
.vscode/extensions.json
View file @
2493bb0f
{
{
"recommendations"
:
[
"recommendations"
:
[
"golang.go"
]
"golang.go"
]
}
}
.vscode/project.code-workspace
View file @
2493bb0f
...
@@ -7,6 +7,6 @@
...
@@ -7,6 +7,6 @@
{
{
"name": "web",
"name": "web",
"path": "../web"
"path": "../web"
}
,
}
]
,
]
}
}
api/system.go
View file @
2493bb0f
...
@@ -18,4 +18,5 @@ type SystemStatus struct {
...
@@ -18,4 +18,5 @@ type SystemStatus struct {
AdditionalScript
string
`json:"additionalScript"`
AdditionalScript
string
`json:"additionalScript"`
// Customized server profile, including server name and external url.
// Customized server profile, including server name and external url.
CustomizedProfile
CustomizedProfile
`json:"customizedProfile"`
CustomizedProfile
CustomizedProfile
`json:"customizedProfile"`
StorageServiceID
int
`json:"storageServiceId"`
}
}
api/system_setting.go
View file @
2493bb0f
...
@@ -25,8 +25,8 @@ const (
...
@@ -25,8 +25,8 @@ const (
SystemSettingAdditionalScriptName
SystemSettingName
=
"additionalScript"
SystemSettingAdditionalScriptName
SystemSettingName
=
"additionalScript"
// SystemSettingCustomizedProfileName is the key type of customized server profile.
// SystemSettingCustomizedProfileName is the key type of customized server profile.
SystemSettingCustomizedProfileName
SystemSettingName
=
"customizedProfile"
SystemSettingCustomizedProfileName
SystemSettingName
=
"customizedProfile"
// SystemSettingStorageService
Name is the key type of sotrage service name
.
// SystemSettingStorageService
ID is the key type of sotrage service ID
.
SystemSettingStorageService
Name
SystemSettingName
=
"storageServiceName
"
SystemSettingStorageService
ID
SystemSettingName
=
"storageServiceId
"
)
)
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
...
@@ -61,8 +61,8 @@ func (key SystemSettingName) String() string {
...
@@ -61,8 +61,8 @@ func (key SystemSettingName) String() string {
return
"additionalScript"
return
"additionalScript"
case
SystemSettingCustomizedProfileName
:
case
SystemSettingCustomizedProfileName
:
return
"customizedProfile"
return
"customizedProfile"
case
SystemSettingStorageService
Name
:
case
SystemSettingStorageService
ID
:
return
"storageService
Name
"
return
"storageService
Id
"
}
}
return
""
return
""
}
}
...
@@ -154,7 +154,7 @@ func (upsert SystemSettingUpsert) Validate() error {
...
@@ -154,7 +154,7 @@ func (upsert SystemSettingUpsert) Validate() error {
if
!
slices
.
Contains
(
UserSettingAppearanceValue
,
customizedProfile
.
Appearance
)
{
if
!
slices
.
Contains
(
UserSettingAppearanceValue
,
customizedProfile
.
Appearance
)
{
return
fmt
.
Errorf
(
"invalid appearance value"
)
return
fmt
.
Errorf
(
"invalid appearance value"
)
}
}
}
else
if
upsert
.
Name
==
SystemSettingStorageService
Name
{
}
else
if
upsert
.
Name
==
SystemSettingStorageService
ID
{
return
nil
return
nil
}
else
{
}
else
{
return
fmt
.
Errorf
(
"invalid system setting name"
)
return
fmt
.
Errorf
(
"invalid system setting name"
)
...
...
server/resource.go
View file @
2493bb0f
...
@@ -86,12 +86,25 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
...
@@ -86,12 +86,25 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
defer
src
.
Close
()
defer
src
.
Close
()
var
resourceCreate
*
api
.
ResourceCreate
var
resourceCreate
*
api
.
ResourceCreate
systemSettingStorageServiceName
:=
api
.
SystemSettingStorageService
Name
systemSettingStorageServiceName
:=
api
.
SystemSettingStorageService
ID
systemSetting
,
err
:=
s
.
Store
.
FindSystemSetting
(
ctx
,
&
api
.
SystemSettingFind
{
Name
:
&
systemSettingStorageServiceName
})
systemSetting
,
err
:=
s
.
Store
.
FindSystemSetting
(
ctx
,
&
api
.
SystemSettingFind
{
Name
:
&
systemSettingStorageServiceName
})
if
err
!=
nil
&&
common
.
ErrorCode
(
err
)
!=
common
.
NotFound
{
if
err
!=
nil
&&
common
.
ErrorCode
(
err
)
!=
common
.
NotFound
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find storage"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find storage"
)
.
SetInternal
(
err
)
}
}
if
common
.
ErrorCode
(
err
)
==
common
.
NotFound
||
systemSetting
.
Value
==
""
{
storeLocal
:=
false
if
common
.
ErrorCode
(
err
)
==
common
.
NotFound
{
storeLocal
=
true
}
else
{
var
value
int
err
=
json
.
Unmarshal
([]
byte
(
systemSetting
.
Value
),
&
value
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to unmarshal storage service id"
)
.
SetInternal
(
err
)
}
if
value
==
0
{
storeLocal
=
true
}
}
if
storeLocal
{
fileBytes
,
err
:=
io
.
ReadAll
(
src
)
fileBytes
,
err
:=
io
.
ReadAll
(
src
)
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to read file"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to read file"
)
.
SetInternal
(
err
)
...
@@ -104,7 +117,12 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
...
@@ -104,7 +117,12 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
Blob
:
fileBytes
,
Blob
:
fileBytes
,
}
}
}
else
{
}
else
{
storage
,
err
:=
s
.
Store
.
FindStorage
(
ctx
,
&
api
.
StorageFind
{
Name
:
&
systemSetting
.
Value
})
storageID
,
err
:=
strconv
.
Atoi
(
systemSetting
.
Value
)
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to convert storageID"
)
.
SetInternal
(
err
)
}
storage
,
err
:=
s
.
Store
.
FindStorage
(
ctx
,
&
api
.
StorageFind
{
ID
:
&
storageID
})
if
err
!=
nil
{
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find storage"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find storage"
)
.
SetInternal
(
err
)
}
}
...
...
server/system.go
View file @
2493bb0f
...
@@ -57,6 +57,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
...
@@ -57,6 +57,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
Appearance
:
"system"
,
Appearance
:
"system"
,
ExternalURL
:
""
,
ExternalURL
:
""
,
},
},
StorageServiceID
:
0
,
}
}
systemSettingList
,
err
:=
s
.
Store
.
FindSystemSettingList
(
ctx
,
&
api
.
SystemSettingFind
{})
systemSettingList
,
err
:=
s
.
Store
.
FindSystemSettingList
(
ctx
,
&
api
.
SystemSettingFind
{})
...
@@ -64,7 +65,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
...
@@ -64,7 +65,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find system setting list"
)
.
SetInternal
(
err
)
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to find system setting list"
)
.
SetInternal
(
err
)
}
}
for
_
,
systemSetting
:=
range
systemSettingList
{
for
_
,
systemSetting
:=
range
systemSettingList
{
if
systemSetting
.
Name
==
api
.
SystemSettingServerID
||
systemSetting
.
Name
==
api
.
SystemSettingSecretSessionName
||
systemSetting
.
Name
==
api
.
SystemSettingStorageServiceName
{
if
systemSetting
.
Name
==
api
.
SystemSettingServerID
||
systemSetting
.
Name
==
api
.
SystemSettingSecretSessionName
{
continue
continue
}
}
...
@@ -103,6 +104,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
...
@@ -103,6 +104,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
if
v
:=
valueMap
[
"externalUrl"
];
v
!=
nil
{
if
v
:=
valueMap
[
"externalUrl"
];
v
!=
nil
{
systemStatus
.
CustomizedProfile
.
ExternalURL
=
v
.
(
string
)
systemStatus
.
CustomizedProfile
.
ExternalURL
=
v
.
(
string
)
}
}
}
else
if
systemSetting
.
Name
==
api
.
SystemSettingStorageServiceID
{
systemStatus
.
StorageServiceID
=
int
(
value
.
(
float64
))
}
}
}
}
...
...
store/storage.go
View file @
2493bb0f
...
@@ -227,6 +227,9 @@ func patchStorageRaw(ctx context.Context, tx *sql.Tx, patch *api.StoragePatch) (
...
@@ -227,6 +227,9 @@ func patchStorageRaw(ctx context.Context, tx *sql.Tx, patch *api.StoragePatch) (
func
findStorageRawList
(
ctx
context
.
Context
,
tx
*
sql
.
Tx
,
find
*
api
.
StorageFind
)
([]
*
storageRaw
,
error
)
{
func
findStorageRawList
(
ctx
context
.
Context
,
tx
*
sql
.
Tx
,
find
*
api
.
StorageFind
)
([]
*
storageRaw
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
interface
{}{}
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
interface
{}{}
if
v
:=
find
.
ID
;
v
!=
nil
{
where
,
args
=
append
(
where
,
"id = ?"
),
append
(
args
,
*
v
)
}
if
v
:=
find
.
Name
;
v
!=
nil
{
if
v
:=
find
.
Name
;
v
!=
nil
{
where
,
args
=
append
(
where
,
"name = ?"
),
append
(
args
,
*
v
)
where
,
args
=
append
(
where
,
"name = ?"
),
append
(
args
,
*
v
)
}
}
...
...
web/src/components/CreateStorageServiceDialog.tsx
0 → 100644
View file @
2493bb0f
import
{
useTranslation
}
from
"react-i18next"
;
import
Icon
from
"./Icon"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
{
Button
,
Input
,
Typography
}
from
"@mui/joy"
;
import
{
useState
}
from
"react"
;
import
{
useStorageStore
}
from
"../store/module"
;
import
toastHelper
from
"./Toast"
;
type
Props
=
DialogProps
;
const
CreateStorageServiceDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
destroy
}
=
props
;
const
{
t
}
=
useTranslation
();
const
storageStore
=
useStorageStore
();
const
[
storageCreate
,
setStorageCreate
]
=
useState
<
StorageCreate
>
({
name
:
""
,
endPoint
:
""
,
region
:
""
,
accessKey
:
""
,
secretKey
:
""
,
bucket
:
""
,
urlPrefix
:
""
,
});
const
handleCloseBtnClick
=
()
=>
{
destroy
();
};
const
allowConfirmAction
=
()
=>
{
if
(
storageCreate
.
name
===
""
||
storageCreate
.
endPoint
===
""
||
storageCreate
.
region
===
""
||
storageCreate
.
accessKey
===
""
||
storageCreate
.
bucket
===
""
||
storageCreate
.
bucket
===
""
)
{
return
false
;
}
return
true
;
};
const
handleConfirmBtnClick
=
async
()
=>
{
try
{
await
storageStore
.
createStorage
(
storageCreate
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
destroy
();
};
const
handleNameChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
name
=
event
.
target
.
value
;
setStorageCreate
({
...
storageCreate
,
name
,
});
};
const
handleEndPointChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
endPoint
=
event
.
target
.
value
;
setStorageCreate
({
...
storageCreate
,
endPoint
,
});
};
const
handleRegionChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
region
=
event
.
target
.
value
;
setStorageCreate
({
...
storageCreate
,
region
,
});
};
const
handleAccessKeyChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
accessKey
=
event
.
target
.
value
;
setStorageCreate
({
...
storageCreate
,
accessKey
,
});
};
const
handleSecretKeyChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
secretKey
=
event
.
target
.
value
;
setStorageCreate
({
...
storageCreate
,
secretKey
,
});
};
const
handleBucketChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
bucket
=
event
.
target
.
value
;
setStorageCreate
({
...
storageCreate
,
bucket
,
});
};
const
handleURLPrefixChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
urlPrefix
=
event
.
target
.
value
;
setStorageCreate
({
...
storageCreate
,
urlPrefix
,
});
};
return
(
<>
<
div
className=
"dialog-header-container !w-64"
>
<
p
className=
"title-text"
>
{
t
(
"setting.storage-section.create-a-service"
)
}
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
handleCloseBtnClick
}
>
<
Icon
.
X
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container"
>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Name
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Name"
value=
{
storageCreate
.
name
}
onChange=
{
handleNameChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
EndPoint
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"EndPoint"
value=
{
storageCreate
.
endPoint
}
onChange=
{
handleEndPointChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Region
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Region"
value=
{
storageCreate
.
region
}
onChange=
{
handleRegionChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
AccessKey
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"AccessKey"
value=
{
storageCreate
.
accessKey
}
onChange=
{
handleAccessKeyChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
SecretKey
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"SecretKey"
value=
{
storageCreate
.
secretKey
}
onChange=
{
handleSecretKeyChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Bucket
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Bucket"
value=
{
storageCreate
.
bucket
}
onChange=
{
handleBucketChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
URLPrefix
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"URLPrefix"
value=
{
storageCreate
.
urlPrefix
}
onChange=
{
handleURLPrefixChange
}
fullWidth
/>
<
div
className=
"mt-2 w-full flex flex-row justify-end items-center space-x-1"
>
<
Button
variant=
"plain"
color=
"neutral"
onClick=
{
handleCloseBtnClick
}
>
Cancel
</
Button
>
<
Button
onClick=
{
handleConfirmBtnClick
}
disabled=
{
!
allowConfirmAction
()
}
>
Create
</
Button
>
</
div
>
</
div
>
</>
);
};
function
showCreateStorageServiceDialog
()
{
generateDialog
(
{
className
:
"create-storage-service-dialog"
,
dialogName
:
"create-storage-service-dialog"
,
},
CreateStorageServiceDialog
);
}
export
default
showCreateStorageServiceDialog
;
web/src/components/SettingDialog.tsx
View file @
2493bb0f
...
@@ -7,11 +7,12 @@ import MyAccountSection from "./Settings/MyAccountSection";
...
@@ -7,11 +7,12 @@ import MyAccountSection from "./Settings/MyAccountSection";
import
PreferencesSection
from
"./Settings/PreferencesSection"
;
import
PreferencesSection
from
"./Settings/PreferencesSection"
;
import
MemberSection
from
"./Settings/MemberSection"
;
import
MemberSection
from
"./Settings/MemberSection"
;
import
SystemSection
from
"./Settings/SystemSection"
;
import
SystemSection
from
"./Settings/SystemSection"
;
import
StorageSection
from
"./Settings/StorageSection"
;
import
"../less/setting-dialog.less"
;
import
"../less/setting-dialog.less"
;
type
Props
=
DialogProps
;
type
Props
=
DialogProps
;
type
SettingSection
=
"my-account"
|
"preferences"
|
"member"
|
"system"
;
type
SettingSection
=
"my-account"
|
"preferences"
|
"
storage"
|
"
member"
|
"system"
;
interface
State
{
interface
State
{
selectedSection
:
SettingSection
;
selectedSection
:
SettingSection
;
...
@@ -57,6 +58,12 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
...
@@ -57,6 +58,12 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
<>
<>
<
span
className=
"section-title"
>
{
t
(
"common.admin"
)
}
</
span
>
<
span
className=
"section-title"
>
{
t
(
"common.admin"
)
}
</
span
>
<
div
className=
"section-items-container"
>
<
div
className=
"section-items-container"
>
<
span
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"storage"
)
}
className=
{
`section-item ${state.selectedSection === "storage" ? "selected" : ""}`
}
>
<
span
className=
"icon-text"
>
🗃️
</
span
>
{
t
(
"setting.storage"
)
}
</
span
>
<
span
<
span
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"member"
)
}
onClick=
{
()
=>
handleSectionSelectorItemClick
(
"member"
)
}
className=
{
`section-item ${state.selectedSection === "member" ? "selected" : ""}`
}
className=
{
`section-item ${state.selectedSection === "member" ? "selected" : ""}`
}
...
@@ -78,6 +85,8 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
...
@@ -78,6 +85,8 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
<
MyAccountSection
/>
<
MyAccountSection
/>
)
:
state
.
selectedSection
===
"preferences"
?
(
)
:
state
.
selectedSection
===
"preferences"
?
(
<
PreferencesSection
/>
<
PreferencesSection
/>
)
:
state
.
selectedSection
===
"storage"
?
(
<
StorageSection
/>
)
:
state
.
selectedSection
===
"member"
?
(
)
:
state
.
selectedSection
===
"member"
?
(
<
MemberSection
/>
<
MemberSection
/>
)
:
state
.
selectedSection
===
"system"
?
(
)
:
state
.
selectedSection
===
"system"
?
(
...
...
web/src/components/Settings/StorageSection.tsx
0 → 100644
View file @
2493bb0f
import
{
Radio
}
from
"@mui/joy"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useGlobalStore
,
useStorageStore
}
from
"../../store/module"
;
import
*
as
api
from
"../../helpers/api"
;
import
showCreateStorageServiceDialog
from
"../CreateStorageServiceDialog"
;
import
showUpdateStorageServiceDialog
from
"../UpdateStorageServiceDialog"
;
import
"../../less/settings/storage-section.less"
;
const
StorageSection
=
()
=>
{
const
{
t
}
=
useTranslation
();
const
storageStore
=
useStorageStore
();
const
storages
=
storageStore
.
state
.
storages
;
const
globalStore
=
useGlobalStore
();
const
systemStatus
=
globalStore
.
state
.
systemStatus
;
const
[
storageServiceId
,
setStorageServiceId
]
=
useState
(
systemStatus
.
storageServiceId
);
useEffect
(()
=>
{
storageStore
.
fetchStorages
();
globalStore
.
fetchSystemStatus
();
},
[]);
useEffect
(()
=>
{
setStorageServiceId
(
systemStatus
.
storageServiceId
);
},
[
systemStatus
]);
const
handleActiveStorageServiceChanged
=
async
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
value
=
parseInt
(
event
.
target
.
value
);
setStorageServiceId
(
value
);
await
api
.
upsertSystemSetting
({
name
:
"storageServiceId"
,
value
:
JSON
.
stringify
(
value
),
});
};
const
handleStorageServiceUpdate
=
async
(
event
:
React
.
MouseEvent
,
storage
:
Storage
)
=>
{
event
.
preventDefault
();
showUpdateStorageServiceDialog
(
storage
);
};
return
(
<
div
className=
"section-container storage-section-container"
>
<
p
className=
"title-text"
>
{
t
(
"setting.storage-section.storage-services-list"
)
}
</
p
>
{
storages
.
map
((
storage
)
=>
(
<
label
className=
"form-label selector"
key=
{
storage
.
id
}
>
<
span
className=
"normal-text underline cursor-pointer"
onClick=
{
(
event
)
=>
handleStorageServiceUpdate
(
event
,
storage
)
}
>
{
storage
.
name
}
</
span
>
<
Radio
value=
{
storage
.
id
}
checked=
{
storageServiceId
===
storage
.
id
}
onChange=
{
handleActiveStorageServiceChanged
}
/>
</
label
>
))
}
<
label
className=
"form-label selector"
>
<
span
className=
"normal-text"
>
{
t
(
"common.database"
)
}
</
span
>
<
Radio
value=
{
0
}
checked=
{
storageServiceId
===
0
}
onChange=
{
handleActiveStorageServiceChanged
}
/>
</
label
>
<
div
className=
"w-full flex flex-row justify-end items-center mt-2 space-x-2"
>
<
button
className=
"btn-normal"
onClick=
{
showCreateStorageServiceDialog
}
>
{
t
(
"setting.storage-section.create-a-service"
)
}
</
button
>
</
div
>
</
div
>
);
};
export
default
StorageSection
;
web/src/components/UpdateStorageServiceDialog.tsx
0 → 100644
View file @
2493bb0f
import
{
Button
,
Input
,
Typography
}
from
"@mui/joy"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
useState
}
from
"react"
;
import
Icon
from
"./Icon"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
{
showCommonDialog
}
from
"./Dialog/CommonDialog"
;
import
{
useStorageStore
}
from
"../store/module"
;
import
toastHelper
from
"./Toast"
;
interface
Props
extends
DialogProps
{
storage
:
StoragePatch
;
}
const
UpdateStorageServiceDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
storage
,
destroy
}
=
props
;
const
{
t
}
=
useTranslation
();
const
storageStore
=
useStorageStore
();
const
[
storagePatch
,
setStoragePatch
]
=
useState
<
StoragePatch
>
(
storage
);
const
handleCloseBtnClick
=
()
=>
{
destroy
();
};
const
allowConfirmAction
=
()
=>
{
if
(
storagePatch
.
name
===
""
||
storagePatch
.
endPoint
===
""
||
storagePatch
.
region
===
""
||
storagePatch
.
accessKey
===
""
||
storagePatch
.
bucket
===
""
||
storagePatch
.
bucket
===
""
)
{
return
false
;
}
return
true
;
};
const
handleConfirmBtnClick
=
async
()
=>
{
try
{
await
storageStore
.
patchStorage
(
storagePatch
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
destroy
();
};
const
handleDeleteBtnClick
=
async
()
=>
{
const
warningText
=
t
(
"setting.storage-section.warning-text"
);
showCommonDialog
({
title
:
t
(
"setting.storage-section.delete-storage"
),
content
:
warningText
,
style
:
"warning"
,
dialogName
:
"delete-storage-dialog"
,
onConfirm
:
async
()
=>
{
try
{
await
storageStore
.
deleteStorageById
(
storagePatch
.
id
);
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toastHelper
.
error
(
error
.
response
.
data
.
message
);
}
destroy
();
},
});
};
const
handleNameChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
name
=
event
.
target
.
value
;
setStoragePatch
({
...
storagePatch
,
name
,
});
};
const
handleEndPointChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
endPoint
=
event
.
target
.
value
;
setStoragePatch
({
...
storagePatch
,
endPoint
,
});
};
const
handleRegionChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
region
=
event
.
target
.
value
;
setStoragePatch
({
...
storagePatch
,
region
,
});
};
const
handleAccessKeyChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
accessKey
=
event
.
target
.
value
;
setStoragePatch
({
...
storagePatch
,
accessKey
,
});
};
const
handleSecretKeyChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
secretKey
=
event
.
target
.
value
;
setStoragePatch
({
...
storagePatch
,
secretKey
,
});
};
const
handleBucketChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
bucket
=
event
.
target
.
value
;
setStoragePatch
({
...
storagePatch
,
bucket
,
});
};
const
handleURLPrefixChange
=
(
event
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
urlPrefix
=
event
.
target
.
value
;
setStoragePatch
({
...
storagePatch
,
urlPrefix
,
});
};
return
(
<>
<
div
className=
"dialog-header-container !w-64"
>
<
p
className=
"title-text"
>
{
t
(
"setting.storage-section.update-a-service"
)
}
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
handleCloseBtnClick
}
>
<
Icon
.
X
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container"
>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Name
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Name"
value=
{
storagePatch
.
name
}
onChange=
{
handleNameChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
EndPoint
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"EndPoint"
value=
{
storagePatch
.
endPoint
}
onChange=
{
handleEndPointChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Region
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Region"
value=
{
storagePatch
.
region
}
onChange=
{
handleRegionChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
AccessKey
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"AccessKey"
value=
{
storagePatch
.
accessKey
}
onChange=
{
handleAccessKeyChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
SecretKey
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"SecretKey"
value=
{
storagePatch
.
secretKey
}
onChange=
{
handleSecretKeyChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
Bucket
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"Bucket"
value=
{
storagePatch
.
bucket
}
onChange=
{
handleBucketChange
}
fullWidth
/>
<
Typography
className=
"!mb-1"
level=
"body2"
>
URLPrefix
</
Typography
>
<
Input
className=
"mb-2"
placeholder=
"URLPrefix"
value=
{
storagePatch
.
urlPrefix
}
onChange=
{
handleURLPrefixChange
}
fullWidth
/>
<
div
className=
"mt-2 w-full flex flex-row justify-between items-center space-x-1"
>
<
Button
color=
"danger"
onClick=
{
handleDeleteBtnClick
}
>
Delete
</
Button
>
<
div
className=
"flex justify-end"
>
<
Button
variant=
"plain"
color=
"neutral"
onClick=
{
handleCloseBtnClick
}
>
Cancel
</
Button
>
<
Button
onClick=
{
handleConfirmBtnClick
}
disabled=
{
!
allowConfirmAction
()
}
>
Update
</
Button
>
</
div
>
</
div
>
</
div
>
</>
);
};
function
showUpdateStorageServiceDialog
(
storage
:
Storage
)
{
generateDialog
(
{
className
:
"update-storage-service-dialog"
,
dialogName
:
"update-storage-service-dialog"
,
},
UpdateStorageServiceDialog
,
{
storage
}
);
}
export
default
showUpdateStorageServiceDialog
;
web/src/helpers/api.ts
View file @
2493bb0f
...
@@ -206,6 +206,22 @@ export function deleteTag(tagName: string) {
...
@@ -206,6 +206,22 @@ export function deleteTag(tagName: string) {
});
});
}
}
export
function
getStorageList
()
{
return
axios
.
get
<
ResponseObject
<
Storage
[]
>>
(
`/api/storage`
);
}
export
function
createStorage
(
storageCreate
:
StorageCreate
)
{
return
axios
.
post
<
ResponseObject
<
Storage
>>
(
`/api/storage`
,
storageCreate
);
}
export
function
patchStorage
(
storagePatch
:
StoragePatch
)
{
return
axios
.
patch
<
ResponseObject
<
Storage
>>
(
`/api/storage/
${
storagePatch
.
id
}
`
,
storagePatch
);
}
export
function
deleteStorage
(
storageId
:
StorageId
)
{
return
axios
.
delete
(
`/api/storage/
${
storageId
}
`
);
}
export
async
function
getRepoStarCount
()
{
export
async
function
getRepoStarCount
()
{
const
{
data
}
=
await
axios
.
get
(
`https://api.github.com/repos/usememos/memos`
,
{
const
{
data
}
=
await
axios
.
get
(
`https://api.github.com/repos/usememos/memos`
,
{
headers
:
{
headers
:
{
...
...
web/src/less/settings/storage-section.less
0 → 100644
View file @
2493bb0f
.storage-section-container {
> .title-text {
@apply mt-4 first:mt-1;
}
> .form-label.selector {
@apply mb-2 flex flex-row justify-between items-center;
> .normal-text {
@apply mr-2 text-sm;
}
}
}
\ No newline at end of file
web/src/locales/en.json
View file @
2493bb0f
...
@@ -47,7 +47,8 @@
...
@@ -47,7 +47,8 @@
"image"
:
"Image"
,
"image"
:
"Image"
,
"link"
:
"Link"
,
"link"
:
"Link"
,
"vacuum"
:
"Vacuum"
,
"vacuum"
:
"Vacuum"
,
"select"
:
"Select"
"select"
:
"Select"
,
"database"
:
"Database"
},
},
"slogan"
:
"An open-source, self-hosted memo hub with knowledge management and social networking."
,
"slogan"
:
"An open-source, self-hosted memo hub with knowledge management and social networking."
,
"auth"
:
{
"auth"
:
{
...
@@ -150,6 +151,7 @@
...
@@ -150,6 +151,7 @@
"setting"
:
{
"setting"
:
{
"my-account"
:
"My Account"
,
"my-account"
:
"My Account"
,
"preference"
:
"Preference"
,
"preference"
:
"Preference"
,
"storage"
:
"Storage"
,
"member"
:
"Member"
,
"member"
:
"Member"
,
"member-list"
:
"Member list"
,
"member-list"
:
"Member list"
,
"system"
:
"System"
,
"system"
:
"System"
,
...
@@ -169,6 +171,13 @@
...
@@ -169,6 +171,13 @@
"created_ts"
:
"Created Time"
,
"created_ts"
:
"Created Time"
,
"updated_ts"
:
"Updated Time"
"updated_ts"
:
"Updated Time"
},
},
"storage-section"
:
{
"storage-services-list"
:
"Storage service list"
,
"create-a-service"
:
"Create a service"
,
"update-a-service"
:
"Update a service"
,
"warning-text"
:
"Are you sure to delete this storage service? THIS ACTION IS IRREVERSIABLE❗"
,
"delete-storage"
:
"Delete Storage"
},
"member-section"
:
{
"member-section"
:
{
"create-a-member"
:
"Create a member"
"create-a-member"
:
"Create a member"
},
},
...
...
web/src/locales/zh.json
View file @
2493bb0f
...
@@ -47,7 +47,8 @@
...
@@ -47,7 +47,8 @@
"image"
:
"图片"
,
"image"
:
"图片"
,
"link"
:
"链接"
,
"link"
:
"链接"
,
"vacuum"
:
"清理"
,
"vacuum"
:
"清理"
,
"select"
:
"选择"
"select"
:
"选择"
,
"database"
:
"数据库"
},
},
"slogan"
:
"An open-source, self-hosted memo hub with knowledge management and social networking."
,
"slogan"
:
"An open-source, self-hosted memo hub with knowledge management and social networking."
,
"auth"
:
{
"auth"
:
{
...
@@ -149,6 +150,7 @@
...
@@ -149,6 +150,7 @@
"setting"
:
{
"setting"
:
{
"my-account"
:
"我的账号"
,
"my-account"
:
"我的账号"
,
"preference"
:
"偏好设置"
,
"preference"
:
"偏好设置"
,
"storage"
:
"存储设置"
,
"member"
:
"成员"
,
"member"
:
"成员"
,
"member-list"
:
"成员列表"
,
"member-list"
:
"成员列表"
,
"system"
:
"系统"
,
"system"
:
"系统"
,
...
@@ -161,13 +163,20 @@
...
@@ -161,13 +163,20 @@
"theme"
:
"主题"
,
"theme"
:
"主题"
,
"default-memo-visibility"
:
"默认 Memo 可见性"
,
"default-memo-visibility"
:
"默认 Memo 可见性"
,
"enable-folding-memo"
:
"开启折叠 Memo"
,
"enable-folding-memo"
:
"开启折叠 Memo"
,
"enable-double-click"
:
"开启双击编辑"
,
"enable-double-click"
:
"开启双击编辑"
,
"editor-font-style"
:
"编辑器字体样式"
,
"editor-font-style"
:
"编辑器字体样式"
,
"mobile-editor-style"
:
"移动端编辑器样式"
,
"mobile-editor-style"
:
"移动端编辑器样式"
,
"default-memo-sort-option"
:
"Memo 显示时间"
,
"default-memo-sort-option"
:
"Memo 显示时间"
,
"created_ts"
:
"创建时间"
,
"created_ts"
:
"创建时间"
,
"updated_ts"
:
"更新时间"
"updated_ts"
:
"更新时间"
},
},
"storage-section"
:
{
"storage-services-list"
:
"存储服务列表"
,
"create-a-service"
:
"新建服务"
,
"update-a-service"
:
"更新服务"
,
"delete-storage"
:
"删除存储服务"
,
"warning-text"
:
"确定删除这个存储服务么?此操作不可逆❗"
},
"member-section"
:
{
"member-section"
:
{
"create-a-member"
:
"创建成员"
"create-a-member"
:
"创建成员"
},
},
...
...
web/src/store/index.ts
View file @
2493bb0f
...
@@ -9,6 +9,7 @@ import locationReducer from "./reducer/location";
...
@@ -9,6 +9,7 @@ import locationReducer from "./reducer/location";
import
resourceReducer
from
"./reducer/resource"
;
import
resourceReducer
from
"./reducer/resource"
;
import
dialogReducer
from
"./reducer/dialog"
;
import
dialogReducer
from
"./reducer/dialog"
;
import
tagReducer
from
"./reducer/tag"
;
import
tagReducer
from
"./reducer/tag"
;
import
storageReducer
from
"./reducer/storage"
;
const
store
=
configureStore
({
const
store
=
configureStore
({
reducer
:
{
reducer
:
{
...
@@ -21,6 +22,7 @@ const store = configureStore({
...
@@ -21,6 +22,7 @@ const store = configureStore({
location
:
locationReducer
,
location
:
locationReducer
,
resource
:
resourceReducer
,
resource
:
resourceReducer
,
dialog
:
dialogReducer
,
dialog
:
dialogReducer
,
storage
:
storageReducer
,
},
},
});
});
...
...
web/src/store/module/index.ts
View file @
2493bb0f
...
@@ -7,3 +7,4 @@ export * from "./resource";
...
@@ -7,3 +7,4 @@ export * from "./resource";
export
*
from
"./shortcut"
;
export
*
from
"./shortcut"
;
export
*
from
"./user"
;
export
*
from
"./user"
;
export
*
from
"./dialog"
;
export
*
from
"./dialog"
;
export
*
from
"./storage"
;
web/src/store/module/storage.ts
0 → 100644
View file @
2493bb0f
import
store
,
{
useAppSelector
}
from
".."
;
import
*
as
api
from
"../../helpers/api"
;
import
{
setStorages
,
createStorage
,
patchStorage
,
deleteStorage
}
from
"../reducer/storage"
;
export
const
useStorageStore
=
()
=>
{
const
state
=
useAppSelector
((
state
)
=>
state
.
storage
);
return
{
state
,
getState
:
()
=>
{
return
store
.
getState
().
storage
;
},
fetchStorages
:
async
()
=>
{
const
{
data
}
=
(
await
api
.
getStorageList
()).
data
;
store
.
dispatch
(
setStorages
(
data
));
},
createStorage
:
async
(
storageCreate
:
StorageCreate
)
=>
{
const
{
data
:
storage
}
=
(
await
api
.
createStorage
(
storageCreate
)).
data
;
store
.
dispatch
(
createStorage
(
storage
));
return
storage
;
},
patchStorage
:
async
(
storagePatch
:
StoragePatch
)
=>
{
const
{
data
:
storage
}
=
(
await
api
.
patchStorage
(
storagePatch
)).
data
;
store
.
dispatch
(
patchStorage
(
storage
));
return
storage
;
},
deleteStorageById
:
async
(
storageId
:
StorageId
)
=>
{
await
api
.
deleteStorage
(
storageId
);
store
.
dispatch
(
deleteStorage
(
storageId
));
},
};
};
web/src/store/reducer/storage.ts
0 → 100644
View file @
2493bb0f
import
{
createSlice
,
PayloadAction
}
from
"@reduxjs/toolkit"
;
interface
State
{
storages
:
Storage
[];
}
const
storageSlice
=
createSlice
({
name
:
"storage"
,
initialState
:
{
storages
:
[],
}
as
State
,
reducers
:
{
setStorages
:
(
state
,
action
:
PayloadAction
<
Storage
[]
>
)
=>
{
return
{
...
state
,
storages
:
action
.
payload
,
};
},
createStorage
:
(
state
,
action
:
PayloadAction
<
Storage
>
)
=>
{
return
{
...
state
,
storages
:
[
action
.
payload
].
concat
(
state
.
storages
),
};
},
patchStorage
:
(
state
,
action
:
PayloadAction
<
Partial
<
Storage
>>
)
=>
{
return
{
...
state
,
storages
:
state
.
storages
.
map
((
storage
)
=>
{
if
(
storage
.
id
===
action
.
payload
.
id
)
{
return
{
...
storage
,
...
action
.
payload
,
};
}
else
{
return
storage
;
}
}),
};
},
deleteStorage
:
(
state
,
action
:
PayloadAction
<
StorageId
>
)
=>
{
return
{
...
state
,
storages
:
state
.
storages
.
filter
((
storage
)
=>
{
return
storage
.
id
!==
action
.
payload
;
}),
};
},
},
});
export
const
{
setStorages
,
createStorage
,
patchStorage
,
deleteStorage
}
=
storageSlice
.
actions
;
export
default
storageSlice
.
reducer
;
web/src/types/modules/storage.d.ts
0 → 100644
View file @
2493bb0f
type
StorageId
=
number
;
interface
Storage
{
id
:
StorageId
;
creatorId
:
UserId
;
createdTs
:
TimeStamp
;
updatedTs
:
TimeStamp
;
name
:
string
;
endPoint
:
string
;
region
:
string
;
accessKey
:
string
;
secretKey
:
string
;
bucket
:
string
;
urlPrefix
:
string
;
}
interface
StorageCreate
{
name
:
string
;
endPoint
:
string
;
region
:
string
;
accessKey
:
string
;
secretKey
:
string
;
bucket
:
string
;
urlPrefix
:
string
;
}
interface
StoragePatch
{
id
:
StorageId
;
name
:
string
;
endPoint
:
string
;
region
:
string
;
accessKey
:
string
;
secretKey
:
string
;
bucket
:
string
;
urlPrefix
:
string
;
}
web/src/types/modules/system.d.ts
View file @
2493bb0f
...
@@ -22,6 +22,7 @@ interface SystemStatus {
...
@@ -22,6 +22,7 @@ interface SystemStatus {
additionalStyle
:
string
;
additionalStyle
:
string
;
additionalScript
:
string
;
additionalScript
:
string
;
customizedProfile
:
CustomizedProfile
;
customizedProfile
:
CustomizedProfile
;
storageServiceId
:
number
;
}
}
interface
SystemSetting
{
interface
SystemSetting
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment