chore: add demo mode

This commit is contained in:
Steven 2023-07-14 18:14:14 +08:00
parent af31875e6a
commit bd9daddaef
11 changed files with 196 additions and 7 deletions

View File

@ -82,7 +82,7 @@ func Execute() error {
func init() { func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVarP(&mode, "mode", "m", "dev", `mode of server, can be "prod" or "dev"`) rootCmd.PersistentFlags().StringVarP(&mode, "mode", "m", "demo", `mode of server, can be "prod" or "dev" or "demo"`)
rootCmd.PersistentFlags().IntVarP(&port, "port", "p", 8082, "port of server") rootCmd.PersistentFlags().IntVarP(&port, "port", "p", 8082, "port of server")
rootCmd.PersistentFlags().StringVarP(&data, "data", "d", "", "data directory") rootCmd.PersistentFlags().StringVarP(&data, "data", "d", "", "data directory")
@ -99,7 +99,7 @@ func init() {
panic(err) panic(err)
} }
viper.SetDefault("mode", "dev") viper.SetDefault("mode", "demo")
viper.SetDefault("port", 8082) viper.SetDefault("port", 8082)
viper.SetEnvPrefix("slash") viper.SetEnvPrefix("slash")
} }

View File

@ -58,8 +58,8 @@ func GetProfile() (*Profile, error) {
return nil, err return nil, err
} }
if profile.Mode != "dev" && profile.Mode != "prod" { if profile.Mode != "demo" && profile.Mode != "dev" && profile.Mode != "prod" {
profile.Mode = "dev" profile.Mode = "demo"
} }
if profile.Mode == "prod" && profile.Data == "" { if profile.Mode == "prod" && profile.Data == "" {

View File

@ -15,7 +15,7 @@ var Version = "0.1.0"
var DevVersion = "0.1.0" var DevVersion = "0.1.0"
func GetCurrentVersion(mode string) string { func GetCurrentVersion(mode string) string {
if mode == "dev" { if mode == "dev" || mode == "demo" {
return DevVersion return DevVersion
} }
return Version return Version

View File

@ -19,6 +19,9 @@ import (
//go:embed migration //go:embed migration
var migrationFS embed.FS var migrationFS embed.FS
//go:embed seed
var seedFS embed.FS
type DB struct { type DB struct {
profile *profile.Profile profile *profile.Profile
// sqlite db connection instance // sqlite db connection instance
@ -119,6 +122,12 @@ func (db *DB) Open(ctx context.Context) (err error) {
if err := db.applyLatestSchema(ctx); err != nil { if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err) return fmt.Errorf("failed to apply latest schema: %w", err)
} }
// In demo mode, we should seed the database.
if db.profile.Mode == "demo" {
if err := db.seed(ctx); err != nil {
return fmt.Errorf("failed to seed: %w", err)
}
}
} }
} }
@ -185,6 +194,28 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
return tx.Commit() return tx.Commit()
} }
func (db *DB) seed(ctx context.Context) error {
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
if err != nil {
return fmt.Errorf("failed to read seed files, err: %w", err)
}
sort.Strings(filenames)
// Loop over all seed files and execute them in order.
for _, filename := range filenames {
buf, err := seedFS.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read seed file, filename=%s err=%w", filename, err)
}
stmt := string(buf)
if err := db.execute(ctx, stmt); err != nil {
return fmt.Errorf("seed error: statement:%s err=%w", stmt, err)
}
}
return nil
}
// execute runs a single SQL statement within a transaction. // execute runs a single SQL statement within a transaction.
func (db *DB) execute(ctx context.Context, stmt string) error { func (db *DB) execute(ctx context.Context, stmt string) error {
tx, err := db.DBInstance.Begin() tx, err := db.DBInstance.Begin()

View File

@ -0,0 +1,9 @@
DELETE FROM activity;
DELETE FROM shortcut;
DELETE FROM user_setting;
DELETE FROM user;
DELETE FROM workspace_setting;

View File

@ -0,0 +1,35 @@
INSERT INTO
user (
`id`,
`role`,
`email`,
`nickname`,
`password_hash`
)
VALUES
(
101,
'ADMIN',
'slash@stevenlgtm.com',
'Slasher',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
INSERT INTO
user (
`id`,
`role`,
`email`,
`nickname`,
`password_hash`
)
VALUES
(
102,
'USER',
'steven@usememos.com',
'Steven',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);

View File

@ -0,0 +1,67 @@
INSERT INTO
shortcut (
`id`,
`creator_id`,
`name`,
`link`,
`visibility`
)
VALUES
(
1,
101,
'memos',
'https://usememos.com',
'PUBLIC'
);
INSERT INTO
shortcut (
`id`,
`creator_id`,
`name`,
`link`,
`visibility`
)
VALUES
(
2,
101,
'sqlchat',
'https://www.sqlchat.ai',
'WORKSPACE'
);
INSERT INTO
shortcut (
`id`,
`creator_id`,
`name`,
`link`,
`visibility`
)
VALUES
(
3,
101,
'schema-change',
'https://www.bytebase.com/blog/how-to-handle-database-schema-change/#what-is-a-database-schema-change',
'PUBLIC'
);
INSERT INTO
shortcut (
`id`,
`creator_id`,
`name`,
`link`,
`visibility`
)
VALUES
(
4,
102,
'stevenlgtm',
'https://github.com/boojack',
'PUBLIC'
);

View File

@ -0,0 +1,31 @@
import { globalService } from "../services";
import Icon from "./Icon";
const DemoBanner: React.FC = () => {
const {
workspaceProfile: {
profile: { mode },
},
} = globalService.getState();
const shouldShow = mode === "demo";
if (!shouldShow) return null;
return (
<div className="z-10 flex flex-row items-center justify-center w-full py-2 text-sm sm:text-lg font-medium dark:text-gray-300 bg-white dark:bg-zinc-700 shadow">
<div className="w-full max-w-4xl px-4 flex flex-row justify-between items-center gap-x-3">
<span>A bookmarking and url shortener, save and share your links very easily.</span>
<a
className="shadow flex flex-row justify-center items-center px-2 py-1 rounded-md text-sm sm:text-base text-white bg-blue-600 hover:bg-blue-700"
href="https://github.com/boojack/slash#deploy-with-docker-in-seconds"
target="_blank"
>
Install
<Icon.ExternalLink className="w-4 h-auto ml-1" />
</a>
</div>
</div>
);
};
export default DemoBanner;

View File

@ -95,7 +95,7 @@ const ShortcutView = (props: Props) => {
<div className="flex flex-row justify-end items-center space-x-2"> <div className="flex flex-row justify-end items-center space-x-2">
{havePermission && ( {havePermission && (
<Dropdown <Dropdown
actionsClassName="!w-24" actionsClassName="!w-32"
actions={ actions={
<> <>
<button <button
@ -104,6 +104,12 @@ const ShortcutView = (props: Props) => {
> >
<Icon.Edit className="w-4 h-auto mr-2" /> Edit <Icon.Edit className="w-4 h-auto mr-2" /> Edit
</button> </button>
<button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => setShowAnalyticsDialog(true)}
>
<Icon.BarChart2 className="w-4 h-auto mr-2" /> Analytics
</button>
<button <button
className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60" className="w-full px-2 flex flex-row justify-start items-center text-left leading-8 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
onClick={() => { onClick={() => {

View File

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { Outlet, useNavigate } from "react-router-dom"; import { Outlet, useNavigate } from "react-router-dom";
import useUserStore from "../stores/v1/user"; import useUserStore from "../stores/v1/user";
import Header from "../components/Header"; import Header from "../components/Header";
import DemoBanner from "../components/DemoBanner";
const Root: React.FC = () => { const Root: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -19,6 +20,7 @@ const Root: React.FC = () => {
<> <>
{currentUser && ( {currentUser && (
<div className="w-full h-full flex flex-col justify-start items-start"> <div className="w-full h-full flex flex-col justify-start items-start">
<DemoBanner />
<Header /> <Header />
<Outlet /> <Outlet />
</div> </div>

View File

@ -11,7 +11,10 @@ const SignIn: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const userStore = useUserStore(); const userStore = useUserStore();
const { const {
workspaceProfile: { disallowSignUp }, workspaceProfile: {
disallowSignUp,
profile: { mode },
},
} = useAppSelector((state) => state.global); } = useAppSelector((state) => state.global);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@ -24,6 +27,11 @@ const SignIn: React.FC = () => {
replace: true, replace: true,
}); });
} }
if (mode === "demo") {
setEmail("slash@stevenlgtm.com");
setPassword("secret");
}
}, []); }, []);
const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEmailInputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {