mirror of
https://github.com/aykhans/slash-e.git
synced 2025-07-06 21:22:36 +00:00
Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
f815804f68 | |||
e89358cb0a | |||
bbe2bdffe3 | |||
70f6f30d69 | |||
af9655eeaf | |||
12cf0f8a8c | |||
916423cc89 | |||
3932cabeac | |||
dddb643bed | |||
0f7a771e85 | |||
f8d36ae1ef | |||
8d8b892d2a | |||
8a4e07120f | |||
8de658709c | |||
cb3e3bfaef | |||
4a25fbb2f6 | |||
83970d5d55 | |||
626b0df21c | |||
8f608dc522 | |||
8f982c5695 | |||
94baa04bb1 | |||
1505e9fa56 | |||
cab701f11b | |||
a3743d7ac6 | |||
7715905204 | |||
f770149066 | |||
f3f2218e91 | |||
b3e766926d | |||
6ed9ecffde | |||
c8d8c4e40c | |||
4f94927b5c | |||
f5f8616f2e | |||
033c007654 | |||
0fb5377226 | |||
f0afa13b8d | |||
53df3a9c1c | |||
8faaf8ced1 | |||
67c3bbf1ee | |||
68745ba9e0 | |||
015336b8c3 | |||
82ac6ab985 | |||
898ca70ad1 | |||
5b2a8394d7 | |||
16e17bffb3 | |||
015040cc1d | |||
c8869e67c7 | |||
a9ae7d2e96 | |||
db9034ccf9 | |||
4d1705dca5 | |||
3225e7c47b | |||
328397612c | |||
c846cde5b4 | |||
5c2cb99866 | |||
742c7da2eb | |||
88b247410f | |||
01417943fb | |||
09f7c33135 | |||
fe3b78f844 | |||
0fd54426e6 | |||
690e14e4ed | |||
7795b17fd1 | |||
c7dd4dc3eb | |||
6ee6a5166e | |||
8c753e9557 | |||
6126701025 | |||
8ef7d5f0d0 | |||
fa8d2f6639 | |||
8cd976791e | |||
010271c668 | |||
383d4f27f0 | |||
cb9786ef7c | |||
e936bb6f15 | |||
60c440ae10 | |||
fc8808ce04 | |||
e88327f2a3 | |||
159dfc9446 |
3
.github/workflows/backend-tests.yml
vendored
3
.github/workflows/backend-tests.yml
vendored
@ -25,7 +25,8 @@ jobs:
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
args: -v
|
||||
version: v1.54.1
|
||||
args: --verbose --timeout=3m
|
||||
skip-cache: true
|
||||
|
||||
go-tests:
|
||||
|
10
.github/workflows/extension-test.yml
vendored
10
.github/workflows/extension-test.yml
vendored
@ -15,9 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
- run: buf generate
|
||||
working-directory: proto
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
@ -28,6 +25,8 @@ jobs:
|
||||
cache-dependency-path: "frontend/extension/pnpm-lock.yaml"
|
||||
- run: pnpm install
|
||||
working-directory: frontend/extension
|
||||
- run: pnpm type-gen
|
||||
working-directory: frontend/extension
|
||||
- name: Run eslint check
|
||||
run: pnpm lint
|
||||
working-directory: frontend/extension
|
||||
@ -36,9 +35,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
- run: buf generate
|
||||
working-directory: proto
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
@ -49,6 +45,8 @@ jobs:
|
||||
cache-dependency-path: "frontend/extension/pnpm-lock.yaml"
|
||||
- run: pnpm install
|
||||
working-directory: frontend/extension
|
||||
- run: pnpm type-gen
|
||||
working-directory: frontend/extension
|
||||
- name: Run extension build
|
||||
run: pnpm build
|
||||
working-directory: frontend/extension
|
||||
|
10
.github/workflows/frontend-test.yml
vendored
10
.github/workflows/frontend-test.yml
vendored
@ -15,9 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
- run: buf generate
|
||||
working-directory: proto
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
@ -28,6 +25,8 @@ jobs:
|
||||
cache-dependency-path: "frontend/web/pnpm-lock.yaml"
|
||||
- run: pnpm install
|
||||
working-directory: frontend/web
|
||||
- run: pnpm type-gen
|
||||
working-directory: frontend/web
|
||||
- name: Run eslint check
|
||||
run: pnpm lint
|
||||
working-directory: frontend/web
|
||||
@ -36,9 +35,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
- run: buf generate
|
||||
working-directory: proto
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
@ -49,6 +45,8 @@ jobs:
|
||||
cache-dependency-path: "frontend/web/pnpm-lock.yaml"
|
||||
- run: pnpm install
|
||||
working-directory: frontend/web
|
||||
- run: pnpm type-gen
|
||||
working-directory: frontend/web
|
||||
- name: Run frontend build
|
||||
run: pnpm build
|
||||
working-directory: frontend/web
|
||||
|
24
Dockerfile
24
Dockerfile
@ -1,26 +1,12 @@
|
||||
# Build protobuf.
|
||||
FROM golang:1.21-alpine AS protobuf
|
||||
WORKDIR /protobuf-generate
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN GO111MODULE=on GOBIN=/usr/local/bin go install github.com/bufbuild/buf/cmd/buf@v1.26.1
|
||||
|
||||
WORKDIR /protobuf-generate/proto
|
||||
|
||||
RUN buf generate
|
||||
|
||||
# Build frontend dist.
|
||||
FROM node:18-alpine AS frontend
|
||||
WORKDIR /frontend-build
|
||||
|
||||
COPY ./frontend .
|
||||
COPY . .
|
||||
|
||||
COPY --from=protobuf /protobuf-generate/frontend/web/src/types/proto ./web/src/types/proto
|
||||
WORKDIR /frontend-build/frontend/web
|
||||
|
||||
WORKDIR /frontend-build/web
|
||||
|
||||
RUN corepack enable && pnpm i --frozen-lockfile
|
||||
RUN corepack enable && pnpm i --frozen-lockfile && pnpm type-gen
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
@ -29,9 +15,9 @@ FROM golang:1.21-alpine AS backend
|
||||
WORKDIR /backend-build
|
||||
|
||||
COPY . .
|
||||
COPY --from=frontend /frontend-build/web/dist ./server/dist
|
||||
COPY --from=frontend /frontend-build/frontend/web/dist ./server/dist
|
||||
|
||||
RUN CGO_ENABLED=0 go build -o slash ./cmd/slash/main.go
|
||||
RUN CGO_ENABLED=0 go build -o slash ./bin/slash/main.go
|
||||
|
||||
# Make workspace with above generated files.
|
||||
FROM alpine:latest AS monolithic
|
||||
|
145
LICENSE
145
LICENSE
@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@ -631,40 +629,33 @@ to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<Slash>
|
||||
Copyright (C) <2023> <Steven>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
19
README.md
19
README.md
@ -2,7 +2,9 @@
|
||||
|
||||
<img align="right" src="./resources/logo.png" height="64px" alt="logo">
|
||||
|
||||
**Slash** is an open source, self-hosted bookmarks and link sharing platform. It allows you to organize your links with tags, and share them using custom shortened URLs. Slash also supports team sharing of link libraries for easy collaboration.
|
||||
**Slash** is an open source, self-hosted bookmarks and link sharing platform. It allows you to organize your links with tags, and share them with custom shortened URLs. Slash also supports team sharing of link libraries for easy collaboration.
|
||||
|
||||
🧩 Browser extension(v1.0.0) now available! - [Chrome Web Store](https://chrome.google.com/webstore/detail/slash/ebaiehmkammnacjadffpicipfckgeobg), [Firefox Add-on](https://addons.mozilla.org/firefox/addon/your-slash/)
|
||||
|
||||
<a href="https://demo.slash.yourselfhosted.com">Live Demo</a> • <a href="https://discord.gg/QZqUuUAhDV">Discord</a>
|
||||
|
||||
@ -11,11 +13,14 @@
|
||||
<a href="https://github.com/boojack/slash/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/boojack/slash?logo=github"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://chrome.google.com/webstore/detail/slash/ebaiehmkammnacjadffpicipfckgeobg"><b>🧩 Browser extension now available!</b></a></p>
|
||||
|
||||

|
||||
|
||||
## Background
|
||||
|
||||
In today's workplace, essential information is often scattered across the cloud in the form of links. We understand the frustration of endlessly searching through emails, messages, and websites just to find the right link. Links are notorious for being unwieldy, complex, and easily lost in the shuffle. Remembering and sharing them can be a challenge.
|
||||
|
||||
That's why we developed Slash, a solution that transforms these links into easily accessible, discoverable, and shareable shortcuts(e.g., `s/shortcut`). Say goodbye to link chaos and welcome the organizational ease of Slash into your daily online workflow.
|
||||
|
||||
## Features
|
||||
|
||||
- Create customizable `/s/` short links for any URL.
|
||||
@ -38,8 +43,12 @@ Slash provides a browser extension to help you use your shortcuts in the search
|
||||
|
||||

|
||||
|
||||
Learn more in [The Browser Extension of Slash](https://github.com/boojack/slash/blob/main/docs/install-browser-extension.md).
|
||||
|
||||
### Chromium based browsers
|
||||
|
||||
For Chromium based browsers(Chrome, Edge, Arc, ...), you can install the extension from the [Chrome Web Store](https://chrome.google.com/webstore/detail/slash/ebaiehmkammnacjadffpicipfckgeobg).
|
||||
|
||||
Learn more in [The Browser Extension of Slash](https://github.com/boojack/slash/blob/main/docs/install-browser-extension.md).
|
||||
### Firefox
|
||||
|
||||
For Firefox, you can install the extension from the [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/your-slash/).
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/mssola/useragent"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/boojack/slash/server/metric"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
@ -78,6 +79,7 @@ func (s *APIV1Service) registerAnalyticsRoutes(g *echo.Group) {
|
||||
browserMap[browserName]++
|
||||
}
|
||||
|
||||
metric.Enqueue("shortcut analytics")
|
||||
return c.JSON(http.StatusOK, &AnalysisData{
|
||||
ReferenceData: mapToReferenceInfoSlice(referenceMap),
|
||||
DeviceData: mapToDeviceInfoSlice(deviceMap),
|
||||
@ -94,8 +96,8 @@ func mapToReferenceInfoSlice(m map[string]int) []ReferenceInfo {
|
||||
Count: value,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(referenceInfoSlice, func(i, j ReferenceInfo) bool {
|
||||
return i.Count > j.Count
|
||||
slices.SortFunc(referenceInfoSlice, func(i, j ReferenceInfo) int {
|
||||
return i.Count - j.Count
|
||||
})
|
||||
return referenceInfoSlice
|
||||
}
|
||||
@ -108,8 +110,8 @@ func mapToDeviceInfoSlice(m map[string]int) []DeviceInfo {
|
||||
Count: value,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(deviceInfoSlice, func(i, j DeviceInfo) bool {
|
||||
return i.Count > j.Count
|
||||
slices.SortFunc(deviceInfoSlice, func(i, j DeviceInfo) int {
|
||||
return i.Count - j.Count
|
||||
})
|
||||
return deviceInfoSlice
|
||||
}
|
||||
@ -122,8 +124,8 @@ func mapToBrowserInfoSlice(m map[string]int) []BrowserInfo {
|
||||
Count: value,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(browserInfoSlice, func(i, j BrowserInfo) bool {
|
||||
return i.Count > j.Count
|
||||
slices.SortFunc(browserInfoSlice, func(i, j BrowserInfo) int {
|
||||
return i.Count - j.Count
|
||||
})
|
||||
return browserInfoSlice
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/boojack/slash/api/auth"
|
||||
storepb "github.com/boojack/slash/proto/gen/store"
|
||||
"github.com/boojack/slash/server/metric"
|
||||
"github.com/boojack/slash/server/service/license"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
@ -63,6 +64,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
|
||||
|
||||
cookieExp := time.Now().Add(auth.CookieExpDuration)
|
||||
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
|
||||
metric.Enqueue("user sign in")
|
||||
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
||||
})
|
||||
|
||||
@ -129,6 +131,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
|
||||
|
||||
cookieExp := time.Now().Add(auth.CookieExpDuration)
|
||||
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
|
||||
metric.Enqueue("user sign up")
|
||||
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
||||
})
|
||||
|
||||
|
@ -36,15 +36,15 @@ func extractTokenFromHeader(c echo.Context) (string, error) {
|
||||
}
|
||||
|
||||
func findAccessToken(c echo.Context) string {
|
||||
accessToken := ""
|
||||
// Check the HTTP request header first.
|
||||
accessToken, _ := extractTokenFromHeader(c)
|
||||
if accessToken == "" {
|
||||
// Check the cookie.
|
||||
cookie, _ := c.Cookie(auth.AccessTokenCookieName)
|
||||
if cookie != nil {
|
||||
accessToken = cookie.Value
|
||||
}
|
||||
if accessToken == "" {
|
||||
accessToken, _ = extractTokenFromHeader(c)
|
||||
}
|
||||
|
||||
return accessToken
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ func JWTMiddleware(s *APIV1Service, next echo.HandlerFunc, secret string) echo.H
|
||||
accessToken := findAccessToken(c)
|
||||
if accessToken == "" {
|
||||
// When the request is not authenticated, we allow the user to access the shortcut endpoints for those public shortcuts.
|
||||
if util.HasPrefixes(path, "/s/") && method == http.MethodGet {
|
||||
if util.HasPrefixes(path, "/s/", "/api/v1/user/") && method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token")
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
storepb "github.com/boojack/slash/proto/gen/store"
|
||||
"github.com/boojack/slash/server/metric"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
@ -30,7 +31,7 @@ func (s *APIV1Service) registerRedirectorRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if shortcut == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found shortcut with name: %s", shortcutName))
|
||||
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/404?shortcut=%s", shortcutName))
|
||||
}
|
||||
if shortcut.Visibility != storepb.Visibility_PUBLIC {
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
@ -46,6 +47,7 @@ func (s *APIV1Service) registerRedirectorRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create activity, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
metric.Enqueue("shortcut redirect")
|
||||
return redirectToShortcut(c, shortcut)
|
||||
})
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/boojack/slash/internal/util"
|
||||
storepb "github.com/boojack/slash/proto/gen/store"
|
||||
"github.com/boojack/slash/server/metric"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
@ -121,6 +122,7 @@ func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) {
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to compose shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
metric.Enqueue("shortcut create")
|
||||
return c.JSON(http.StatusOK, shortcutMessage)
|
||||
})
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.deanishe.net/favicon"
|
||||
)
|
||||
|
||||
func (*APIV1Service) registerURLUtilRoutes(g *echo.Group) {
|
||||
// GET /url/favicon?url=...
|
||||
g.GET("/url/favicon", func(c echo.Context) error {
|
||||
url := c.QueryParam("url")
|
||||
icons, err := favicon.Find(url)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to find favicon, err: %s", err))
|
||||
}
|
||||
|
||||
availableIcons := []*favicon.Icon{}
|
||||
for _, icon := range icons {
|
||||
if icon.Width == icon.Height {
|
||||
availableIcons = append(availableIcons, icon)
|
||||
}
|
||||
}
|
||||
if len(availableIcons) == 0 {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "no favicon found")
|
||||
}
|
||||
return c.JSON(http.StatusOK, availableIcons[0].URL)
|
||||
})
|
||||
}
|
@ -11,6 +11,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/boojack/slash/internal/util"
|
||||
"github.com/boojack/slash/server/metric"
|
||||
"github.com/boojack/slash/server/service/license"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
@ -137,6 +138,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
userMessage := convertUserFromStore(user)
|
||||
metric.Enqueue("user create")
|
||||
return c.JSON(http.StatusOK, userMessage)
|
||||
})
|
||||
|
||||
@ -186,7 +188,12 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
||||
userMessage := convertUserFromStore(user)
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
userMessage.Email = ""
|
||||
}
|
||||
return c.JSON(http.StatusOK, userMessage)
|
||||
})
|
||||
|
||||
g.PATCH("/user/:id", func(c echo.Context) error {
|
||||
|
@ -27,7 +27,6 @@ func (s *APIV1Service) Start(apiGroup *echo.Group, secret string) {
|
||||
apiV1Group.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return JWTMiddleware(s, next, secret)
|
||||
})
|
||||
s.registerURLUtilRoutes(apiV1Group)
|
||||
s.registerWorkspaceRoutes(apiV1Group)
|
||||
s.registerAuthRoutes(apiV1Group, secret)
|
||||
s.registerUserRoutes(apiV1Group)
|
||||
|
@ -4,6 +4,9 @@ import "strings"
|
||||
|
||||
var allowedMethodsWhenUnauthorized = map[string]bool{
|
||||
"/slash.api.v2.WorkspaceService/GetWorkspaceProfile": true,
|
||||
"/slash.api.v2.WorkspaceService/GetWorkspaceSetting": true,
|
||||
"/slash.api.v2.ShortcutService/GetShortcut": true,
|
||||
"/slash.api.v2.CollectionService/GetCollectionByName": true,
|
||||
}
|
||||
|
||||
// isUnauthorizeAllowedMethod returns true if the method is allowed to be called when the user is not authorized.
|
||||
|
205
api/v2/collection_service.go
Normal file
205
api/v2/collection_service.go
Normal file
@ -0,0 +1,205 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
apiv2pb "github.com/boojack/slash/proto/gen/api/v2"
|
||||
storepb "github.com/boojack/slash/proto/gen/store"
|
||||
"github.com/boojack/slash/server/metric"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) ListCollections(ctx context.Context, _ *apiv2pb.ListCollectionsRequest) (*apiv2pb.ListCollectionsResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
find := &store.FindCollection{}
|
||||
find.CreatorID = &userID
|
||||
collections, err := s.Store.ListCollections(ctx, find)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get collection list, err: %v", err)
|
||||
}
|
||||
|
||||
convertedCollections := []*apiv2pb.Collection{}
|
||||
for _, collection := range collections {
|
||||
convertedCollections = append(convertedCollections, convertCollectionFromStore(collection))
|
||||
}
|
||||
|
||||
response := &apiv2pb.ListCollectionsResponse{
|
||||
Collections: convertedCollections,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetCollection(ctx context.Context, request *apiv2pb.GetCollectionRequest) (*apiv2pb.GetCollectionResponse, error) {
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
ID: &request.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get collection by name: %v", err)
|
||||
}
|
||||
if collection == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "collection not found")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if collection.Visibility == storepb.Visibility_PRIVATE && collection.CreatorId != userID {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
response := &apiv2pb.GetCollectionResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetCollectionByName(ctx context.Context, request *apiv2pb.GetCollectionByNameRequest) (*apiv2pb.GetCollectionByNameResponse, error) {
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
Name: &request.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get collection by name: %v", err)
|
||||
}
|
||||
if collection == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "collection not found")
|
||||
}
|
||||
|
||||
userID, ok := ctx.Value(userIDContextKey).(int32)
|
||||
if ok {
|
||||
if collection.Visibility == storepb.Visibility_PRIVATE && collection.CreatorId != userID {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
} else {
|
||||
if collection.Visibility != storepb.Visibility_PUBLIC {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
}
|
||||
response := &apiv2pb.GetCollectionByNameResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
metric.Enqueue("collection view")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) CreateCollection(ctx context.Context, request *apiv2pb.CreateCollectionRequest) (*apiv2pb.CreateCollectionResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
collection := &storepb.Collection{
|
||||
CreatorId: userID,
|
||||
Name: request.Collection.Name,
|
||||
Title: request.Collection.Title,
|
||||
Description: request.Collection.Description,
|
||||
ShortcutIds: request.Collection.ShortcutIds,
|
||||
Visibility: storepb.Visibility(request.Collection.Visibility),
|
||||
}
|
||||
collection, err := s.Store.CreateCollection(ctx, collection)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create collection, err: %v", err)
|
||||
}
|
||||
|
||||
response := &apiv2pb.CreateCollectionResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
metric.Enqueue("collection create")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateCollection(ctx context.Context, request *apiv2pb.UpdateCollectionRequest) (*apiv2pb.UpdateCollectionResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "updateMask is required")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
}
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
ID: &request.Collection.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get collection by name: %v", err)
|
||||
}
|
||||
if collection == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "collection not found")
|
||||
}
|
||||
if collection.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
update := &store.UpdateCollection{
|
||||
ID: collection.Id,
|
||||
}
|
||||
for _, path := range request.UpdateMask.Paths {
|
||||
switch path {
|
||||
case "name":
|
||||
update.Name = &request.Collection.Name
|
||||
case "title":
|
||||
update.Title = &request.Collection.Title
|
||||
case "description":
|
||||
update.Description = &request.Collection.Description
|
||||
case "shortcut_ids":
|
||||
update.ShortcutIDs = request.Collection.ShortcutIds
|
||||
case "visibility":
|
||||
visibility := store.Visibility(request.Collection.Visibility.String())
|
||||
update.Visibility = &visibility
|
||||
}
|
||||
}
|
||||
collection, err = s.Store.UpdateCollection(ctx, update)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to update collection, err: %v", err)
|
||||
}
|
||||
|
||||
response := &apiv2pb.UpdateCollectionResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteCollection(ctx context.Context, request *apiv2pb.DeleteCollectionRequest) (*apiv2pb.DeleteCollectionResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
}
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
ID: &request.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get collection by name: %v", err)
|
||||
}
|
||||
if collection == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "collection not found")
|
||||
}
|
||||
if collection.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
err = s.Store.DeleteCollection(ctx, &store.DeleteCollection{
|
||||
ID: collection.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to delete collection, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.DeleteCollectionResponse{}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func convertCollectionFromStore(collection *storepb.Collection) *apiv2pb.Collection {
|
||||
return &apiv2pb.Collection{
|
||||
Id: collection.Id,
|
||||
CreatorId: collection.CreatorId,
|
||||
CreatedTime: timestamppb.New(time.Unix(collection.CreatedTs, 0)),
|
||||
UpdatedTime: timestamppb.New(time.Unix(collection.UpdatedTs, 0)),
|
||||
Name: collection.Name,
|
||||
Title: collection.Title,
|
||||
Description: collection.Description,
|
||||
ShortcutIds: collection.ShortcutIds,
|
||||
Visibility: apiv2pb.Visibility(collection.Visibility),
|
||||
}
|
||||
}
|
@ -2,33 +2,22 @@ package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
apiv2pb "github.com/boojack/slash/proto/gen/api/v2"
|
||||
storepb "github.com/boojack/slash/proto/gen/store"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
type ShortcutService struct {
|
||||
apiv2pb.UnimplementedShortcutServiceServer
|
||||
|
||||
Secret string
|
||||
Store *store.Store
|
||||
}
|
||||
|
||||
// NewShortcutService creates a new Shortcut service.
|
||||
func NewShortcutService(secret string, store *store.Store) *ShortcutService {
|
||||
return &ShortcutService{
|
||||
Secret: secret,
|
||||
Store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ShortcutService) ListShortcuts(ctx context.Context, _ *apiv2pb.ListShortcutsRequest) (*apiv2pb.ListShortcutsResponse, error) {
|
||||
func (s *APIV2Service) ListShortcuts(ctx context.Context, _ *apiv2pb.ListShortcutsRequest) (*apiv2pb.ListShortcutsResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
find := &store.FindShortcut{}
|
||||
find.VisibilityList = []store.Visibility{store.VisibilityWorkspace, store.VisibilityPublic}
|
||||
@ -47,7 +36,11 @@ func (s *ShortcutService) ListShortcuts(ctx context.Context, _ *apiv2pb.ListShor
|
||||
shortcutList = append(shortcutList, visibleShortcutList...)
|
||||
shortcuts := []*apiv2pb.Shortcut{}
|
||||
for _, shortcut := range shortcutList {
|
||||
shortcuts = append(shortcuts, convertShortcutFromStorepb(shortcut))
|
||||
composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
shortcuts = append(shortcuts, composedShortcut)
|
||||
}
|
||||
|
||||
response := &apiv2pb.ListShortcutsResponse{
|
||||
@ -56,9 +49,9 @@ func (s *ShortcutService) ListShortcuts(ctx context.Context, _ *apiv2pb.ListShor
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *ShortcutService) GetShortcut(ctx context.Context, request *apiv2pb.GetShortcutRequest) (*apiv2pb.GetShortcutResponse, error) {
|
||||
func (s *APIV2Service) GetShortcut(ctx context.Context, request *apiv2pb.GetShortcutRequest) (*apiv2pb.GetShortcutResponse, error) {
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
Name: &request.Name,
|
||||
ID: &request.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get shortcut by name: %v", err)
|
||||
@ -67,18 +60,28 @@ func (s *ShortcutService) GetShortcut(ctx context.Context, request *apiv2pb.GetS
|
||||
return nil, status.Errorf(codes.NotFound, "shortcut not found")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
userID, ok := ctx.Value(userIDContextKey).(int32)
|
||||
if ok {
|
||||
if shortcut.Visibility == storepb.Visibility_PRIVATE && shortcut.CreatorId != userID {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
shortcutMessage := convertShortcutFromStorepb(shortcut)
|
||||
} else {
|
||||
if shortcut.Visibility != storepb.Visibility_PUBLIC {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
}
|
||||
|
||||
composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.GetShortcutResponse{
|
||||
Shortcut: shortcutMessage,
|
||||
Shortcut: composedShortcut,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *ShortcutService) CreateShortcut(ctx context.Context, request *apiv2pb.CreateShortcutRequest) (*apiv2pb.CreateShortcutResponse, error) {
|
||||
func (s *APIV2Service) CreateShortcut(ctx context.Context, request *apiv2pb.CreateShortcutRequest) (*apiv2pb.CreateShortcutResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
shortcut := &storepb.Shortcut{
|
||||
CreatorId: userID,
|
||||
@ -105,13 +108,21 @@ func (s *ShortcutService) CreateShortcut(ctx context.Context, request *apiv2pb.C
|
||||
return nil, status.Errorf(codes.Internal, "failed to create activity, err: %v", err)
|
||||
}
|
||||
|
||||
composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.CreateShortcutResponse{
|
||||
Shortcut: convertShortcutFromStorepb(shortcut),
|
||||
Shortcut: composedShortcut,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *ShortcutService) DeleteShortcut(ctx context.Context, request *apiv2pb.DeleteShortcutRequest) (*apiv2pb.DeleteShortcutResponse, error) {
|
||||
func (s *APIV2Service) UpdateShortcut(ctx context.Context, request *apiv2pb.UpdateShortcutRequest) (*apiv2pb.UpdateShortcutResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "updateMask is required")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
@ -120,7 +131,68 @@ func (s *ShortcutService) DeleteShortcut(ctx context.Context, request *apiv2pb.D
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
}
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
Name: &request.Name,
|
||||
ID: &request.Shortcut.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get shortcut by name: %v", err)
|
||||
}
|
||||
if shortcut == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "shortcut not found")
|
||||
}
|
||||
if shortcut.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
update := &store.UpdateShortcut{}
|
||||
for _, path := range request.UpdateMask.Paths {
|
||||
switch path {
|
||||
case "link":
|
||||
update.Link = &request.Shortcut.Link
|
||||
case "title":
|
||||
update.Title = &request.Shortcut.Title
|
||||
case "tags":
|
||||
tag := strings.Join(request.Shortcut.Tags, " ")
|
||||
update.Tag = &tag
|
||||
case "description":
|
||||
update.Description = &request.Shortcut.Description
|
||||
case "visibility":
|
||||
visibility := store.Visibility(request.Shortcut.Visibility.String())
|
||||
update.Visibility = &visibility
|
||||
case "og_metadata":
|
||||
if request.Shortcut.OgMetadata != nil {
|
||||
update.OpenGraphMetadata = &store.OpenGraphMetadata{
|
||||
Title: request.Shortcut.OgMetadata.Title,
|
||||
Description: request.Shortcut.OgMetadata.Description,
|
||||
Image: request.Shortcut.OgMetadata.Image,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shortcut, err = s.Store.UpdateShortcut(ctx, update)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to update shortcut, err: %v", err)
|
||||
}
|
||||
|
||||
composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.UpdateShortcutResponse{
|
||||
Shortcut: composedShortcut,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteShortcut(ctx context.Context, request *apiv2pb.DeleteShortcutRequest) (*apiv2pb.DeleteShortcutResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
}
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &request.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get shortcut by name: %v", err)
|
||||
@ -142,7 +214,7 @@ func (s *ShortcutService) DeleteShortcut(ctx context.Context, request *apiv2pb.D
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *ShortcutService) createShortcutCreateActivity(ctx context.Context, shortcut *storepb.Shortcut) error {
|
||||
func (s *APIV2Service) createShortcutCreateActivity(ctx context.Context, shortcut *storepb.Shortcut) error {
|
||||
payload := &storepb.ActivityShorcutCreatePayload{
|
||||
ShortcutId: shortcut.Id,
|
||||
}
|
||||
@ -163,12 +235,12 @@ func (s *ShortcutService) createShortcutCreateActivity(ctx context.Context, shor
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertShortcutFromStorepb(shortcut *storepb.Shortcut) *apiv2pb.Shortcut {
|
||||
return &apiv2pb.Shortcut{
|
||||
func (s *APIV2Service) convertShortcutFromStorepb(ctx context.Context, shortcut *storepb.Shortcut) (*apiv2pb.Shortcut, error) {
|
||||
composedShortcut := &apiv2pb.Shortcut{
|
||||
Id: shortcut.Id,
|
||||
CreatorId: shortcut.CreatorId,
|
||||
CreatedTs: shortcut.CreatedTs,
|
||||
UpdatedTs: shortcut.UpdatedTs,
|
||||
CreatedTime: timestamppb.New(time.Unix(shortcut.CreatedTs, 0)),
|
||||
UpdatedTime: timestamppb.New(time.Unix(shortcut.UpdatedTs, 0)),
|
||||
RowStatus: apiv2pb.RowStatus(shortcut.RowStatus),
|
||||
Name: shortcut.Name,
|
||||
Link: shortcut.Link,
|
||||
@ -182,4 +254,16 @@ func convertShortcutFromStorepb(shortcut *storepb.Shortcut) *apiv2pb.Shortcut {
|
||||
Image: shortcut.OgMetadata.Image,
|
||||
},
|
||||
}
|
||||
|
||||
activityList, err := s.Store.ListActivities(ctx, &store.FindActivity{
|
||||
Type: store.ActivityShortcutView,
|
||||
Level: store.ActivityInfo,
|
||||
Where: []string{fmt.Sprintf("json_extract(payload, '$.shortcutId') = %d", composedShortcut.Id)},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to list activities")
|
||||
}
|
||||
composedShortcut.ViewCount = int32(len(activityList))
|
||||
|
||||
return composedShortcut, nil
|
||||
}
|
||||
|
@ -7,29 +7,9 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
apiv2pb "github.com/boojack/slash/proto/gen/api/v2"
|
||||
"github.com/boojack/slash/server/profile"
|
||||
"github.com/boojack/slash/server/service/license"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
type SubscriptionService struct {
|
||||
apiv2pb.UnimplementedSubscriptionServiceServer
|
||||
|
||||
Profile *profile.Profile
|
||||
Store *store.Store
|
||||
LicenseService *license.LicenseService
|
||||
}
|
||||
|
||||
// NewSubscriptionService creates a new SubscriptionService.
|
||||
func NewSubscriptionService(profile *profile.Profile, store *store.Store, licenseService *license.LicenseService) *SubscriptionService {
|
||||
return &SubscriptionService{
|
||||
Profile: profile,
|
||||
Store: store,
|
||||
LicenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) GetSubscription(ctx context.Context, _ *apiv2pb.GetSubscriptionRequest) (*apiv2pb.GetSubscriptionResponse, error) {
|
||||
func (s *APIV2Service) GetSubscription(ctx context.Context, _ *apiv2pb.GetSubscriptionRequest) (*apiv2pb.GetSubscriptionResponse, error) {
|
||||
subscription, err := s.LicenseService.LoadSubscription(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to load subscription: %v", err)
|
||||
@ -39,7 +19,7 @@ func (s *SubscriptionService) GetSubscription(ctx context.Context, _ *apiv2pb.Ge
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) UpdateSubscription(ctx context.Context, request *apiv2pb.UpdateSubscriptionRequest) (*apiv2pb.UpdateSubscriptionResponse, error) {
|
||||
func (s *APIV2Service) UpdateSubscription(ctx context.Context, request *apiv2pb.UpdateSubscriptionRequest) (*apiv2pb.UpdateSubscriptionResponse, error) {
|
||||
subscription, err := s.LicenseService.UpdateSubscription(ctx, request.LicenseKey)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to load subscription: %v", err)
|
||||
|
@ -19,24 +19,7 @@ import (
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
apiv2pb.UnimplementedUserServiceServer
|
||||
|
||||
Secret string
|
||||
Store *store.Store
|
||||
LicenseService *license.LicenseService
|
||||
}
|
||||
|
||||
// NewUserService creates a new UserService.
|
||||
func NewUserService(secret string, store *store.Store, licenseService *license.LicenseService) *UserService {
|
||||
return &UserService{
|
||||
Secret: secret,
|
||||
Store: store,
|
||||
LicenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersRequest) (*apiv2pb.ListUsersResponse, error) {
|
||||
func (s *APIV2Service) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersRequest) (*apiv2pb.ListUsersResponse, error) {
|
||||
users, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
|
||||
@ -52,7 +35,7 @@ func (s *UserService) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersRequest
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetUser(ctx context.Context, request *apiv2pb.GetUserRequest) (*apiv2pb.GetUserResponse, error) {
|
||||
func (s *APIV2Service) GetUser(ctx context.Context, request *apiv2pb.GetUserRequest) (*apiv2pb.GetUserResponse, error) {
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &request.Id,
|
||||
})
|
||||
@ -70,7 +53,7 @@ func (s *UserService) GetUser(ctx context.Context, request *apiv2pb.GetUserReque
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUser(ctx context.Context, request *apiv2pb.CreateUserRequest) (*apiv2pb.CreateUserResponse, error) {
|
||||
func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUserRequest) (*apiv2pb.CreateUserResponse, error) {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to hash password: %v", err)
|
||||
@ -101,19 +84,19 @@ func (s *UserService) CreateUser(ctx context.Context, request *apiv2pb.CreateUse
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUserRequest) (*apiv2pb.UpdateUserResponse, error) {
|
||||
func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUserRequest) (*apiv2pb.UpdateUserResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.User.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
if request.UpdateMask == nil || len(request.UpdateMask) == 0 {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "UpdateMask is empty")
|
||||
}
|
||||
|
||||
userUpdate := &store.UpdateUser{
|
||||
ID: request.User.Id,
|
||||
}
|
||||
for _, path := range request.UpdateMask {
|
||||
for _, path := range request.UpdateMask.Paths {
|
||||
if path == "email" {
|
||||
userUpdate.Email = &request.User.Email
|
||||
} else if path == "nickname" {
|
||||
@ -129,7 +112,7 @@ func (s *UserService) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUse
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserService) DeleteUser(ctx context.Context, request *apiv2pb.DeleteUserRequest) (*apiv2pb.DeleteUserResponse, error) {
|
||||
func (s *APIV2Service) DeleteUser(ctx context.Context, request *apiv2pb.DeleteUserRequest) (*apiv2pb.DeleteUserResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID == request.Id {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "cannot delete yourself")
|
||||
@ -145,7 +128,7 @@ func (s *UserService) DeleteUser(ctx context.Context, request *apiv2pb.DeleteUse
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *UserService) ListUserAccessTokens(ctx context.Context, request *apiv2pb.ListUserAccessTokensRequest) (*apiv2pb.ListUserAccessTokensResponse, error) {
|
||||
func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2pb.ListUserAccessTokensRequest) (*apiv2pb.ListUserAccessTokensResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
@ -187,8 +170,8 @@ func (s *UserService) ListUserAccessTokens(ctx context.Context, request *apiv2pb
|
||||
}
|
||||
|
||||
// Sort by issued time in descending order.
|
||||
slices.SortFunc(accessTokens, func(i, j *apiv2pb.UserAccessToken) bool {
|
||||
return i.IssuedAt.Seconds > j.IssuedAt.Seconds
|
||||
slices.SortFunc(accessTokens, func(i, j *apiv2pb.UserAccessToken) int {
|
||||
return int(i.IssuedAt.Seconds - j.IssuedAt.Seconds)
|
||||
})
|
||||
response := &apiv2pb.ListUserAccessTokensResponse{
|
||||
AccessTokens: accessTokens,
|
||||
@ -196,7 +179,7 @@ func (s *UserService) ListUserAccessTokens(ctx context.Context, request *apiv2pb
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUserAccessToken(ctx context.Context, request *apiv2pb.CreateUserAccessTokenRequest) (*apiv2pb.CreateUserAccessTokenResponse, error) {
|
||||
func (s *APIV2Service) CreateUserAccessToken(ctx context.Context, request *apiv2pb.CreateUserAccessTokenRequest) (*apiv2pb.CreateUserAccessTokenResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
@ -256,7 +239,7 @@ func (s *UserService) CreateUserAccessToken(ctx context.Context, request *apiv2p
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *UserService) DeleteUserAccessToken(ctx context.Context, request *apiv2pb.DeleteUserAccessTokenRequest) (*apiv2pb.DeleteUserAccessTokenResponse, error) {
|
||||
func (s *APIV2Service) DeleteUserAccessToken(ctx context.Context, request *apiv2pb.DeleteUserAccessTokenRequest) (*apiv2pb.DeleteUserAccessTokenResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
@ -288,7 +271,7 @@ func (s *UserService) DeleteUserAccessToken(ctx context.Context, request *apiv2p
|
||||
return &apiv2pb.DeleteUserAccessTokenResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken, description string) error {
|
||||
func (s *APIV2Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken, description string) error {
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get user access tokens")
|
||||
@ -316,8 +299,8 @@ func convertUserFromStore(user *store.User) *apiv2pb.User {
|
||||
return &apiv2pb.User{
|
||||
Id: int32(user.ID),
|
||||
RowStatus: convertRowStatusFromStore(user.RowStatus),
|
||||
CreatedTs: user.CreatedTs,
|
||||
UpdatedTs: user.UpdatedTs,
|
||||
CreatedTime: timestamppb.New(time.Unix(user.CreatedTs, 0)),
|
||||
UpdatedTime: timestamppb.New(time.Unix(user.UpdatedTs, 0)),
|
||||
Role: convertUserRoleFromStore(user.Role),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nickname,
|
||||
|
@ -12,20 +12,7 @@ import (
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
type UserSettingService struct {
|
||||
apiv2pb.UnimplementedUserSettingServiceServer
|
||||
|
||||
Store *store.Store
|
||||
}
|
||||
|
||||
// NewUserSettingService creates a new UserSettingService.
|
||||
func NewUserSettingService(store *store.Store) *UserSettingService {
|
||||
return &UserSettingService{
|
||||
Store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserSettingService) GetUserSetting(ctx context.Context, request *apiv2pb.GetUserSettingRequest) (*apiv2pb.GetUserSettingResponse, error) {
|
||||
func (s *APIV2Service) GetUserSetting(ctx context.Context, request *apiv2pb.GetUserSettingRequest) (*apiv2pb.GetUserSettingResponse, error) {
|
||||
userSetting, err := getUserSetting(ctx, s.Store, request.Id)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err)
|
||||
@ -35,13 +22,13 @@ func (s *UserSettingService) GetUserSetting(ctx context.Context, request *apiv2p
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UserSettingService) UpdateUserSetting(ctx context.Context, request *apiv2pb.UpdateUserSettingRequest) (*apiv2pb.UpdateUserSettingResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask) == 0 {
|
||||
func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.UpdateUserSettingRequest) (*apiv2pb.UpdateUserSettingResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
for _, path := range request.UpdateMask {
|
||||
for _, path := range request.UpdateMask.Paths {
|
||||
if path == "locale" {
|
||||
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
||||
UserId: userID,
|
||||
|
33
api/v2/v2.go
33
api/v2/v2.go
@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
||||
"github.com/labstack/echo/v4"
|
||||
"google.golang.org/grpc"
|
||||
@ -18,6 +18,13 @@ import (
|
||||
)
|
||||
|
||||
type APIV2Service struct {
|
||||
apiv2pb.UnimplementedWorkspaceServiceServer
|
||||
apiv2pb.UnimplementedSubscriptionServiceServer
|
||||
apiv2pb.UnimplementedUserServiceServer
|
||||
apiv2pb.UnimplementedUserSettingServiceServer
|
||||
apiv2pb.UnimplementedShortcutServiceServer
|
||||
apiv2pb.UnimplementedCollectionServiceServer
|
||||
|
||||
Secret string
|
||||
Profile *profile.Profile
|
||||
Store *store.Store
|
||||
@ -34,14 +41,7 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store
|
||||
authProvider.AuthenticationInterceptor,
|
||||
),
|
||||
)
|
||||
apiv2pb.RegisterSubscriptionServiceServer(grpcServer, NewSubscriptionService(profile, store, licenseService))
|
||||
apiv2pb.RegisterWorkspaceServiceServer(grpcServer, NewWorkspaceService(profile, store, licenseService))
|
||||
apiv2pb.RegisterUserServiceServer(grpcServer, NewUserService(secret, store, licenseService))
|
||||
apiv2pb.RegisterUserSettingServiceServer(grpcServer, NewUserSettingService(store))
|
||||
apiv2pb.RegisterShortcutServiceServer(grpcServer, NewShortcutService(secret, store))
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
return &APIV2Service{
|
||||
apiV2Service := &APIV2Service{
|
||||
Secret: secret,
|
||||
Profile: profile,
|
||||
Store: store,
|
||||
@ -49,6 +49,16 @@ func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store
|
||||
grpcServer: grpcServer,
|
||||
grpcServerPort: grpcServerPort,
|
||||
}
|
||||
|
||||
apiv2pb.RegisterSubscriptionServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterWorkspaceServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterUserServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterUserSettingServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterShortcutServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterCollectionServiceServer(grpcServer, apiV2Service)
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
return apiV2Service
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetGRPCServer() *grpc.Server {
|
||||
@ -68,7 +78,7 @@ func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error
|
||||
return err
|
||||
}
|
||||
|
||||
gwMux := grpcRuntime.NewServeMux()
|
||||
gwMux := runtime.NewServeMux()
|
||||
if err := apiv2pb.RegisterSubscriptionServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -84,6 +94,9 @@ func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error
|
||||
if err := apiv2pb.RegisterShortcutServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterCollectionServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Any("/api/v2/*", echo.WrapHandler(gwMux))
|
||||
|
||||
// GRPC web proxy.
|
||||
|
@ -8,29 +8,10 @@ import (
|
||||
|
||||
apiv2pb "github.com/boojack/slash/proto/gen/api/v2"
|
||||
storepb "github.com/boojack/slash/proto/gen/store"
|
||||
"github.com/boojack/slash/server/profile"
|
||||
"github.com/boojack/slash/server/service/license"
|
||||
"github.com/boojack/slash/store"
|
||||
)
|
||||
|
||||
type WorkspaceService struct {
|
||||
apiv2pb.UnimplementedWorkspaceServiceServer
|
||||
|
||||
Profile *profile.Profile
|
||||
Store *store.Store
|
||||
LicenseService *license.LicenseService
|
||||
}
|
||||
|
||||
// NewWorkspaceService creates a new WorkspaceService.
|
||||
func NewWorkspaceService(profile *profile.Profile, store *store.Store, licenseService *license.LicenseService) *WorkspaceService {
|
||||
return &WorkspaceService{
|
||||
Profile: profile,
|
||||
Store: store,
|
||||
LicenseService: licenseService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WorkspaceService) GetWorkspaceProfile(ctx context.Context, _ *apiv2pb.GetWorkspaceProfileRequest) (*apiv2pb.GetWorkspaceProfileResponse, error) {
|
||||
func (s *APIV2Service) GetWorkspaceProfile(ctx context.Context, _ *apiv2pb.GetWorkspaceProfileRequest) (*apiv2pb.GetWorkspaceProfileResponse, error) {
|
||||
profile := &apiv2pb.WorkspaceProfile{
|
||||
Mode: s.Profile.Mode,
|
||||
Plan: apiv2pb.PlanType_FREE,
|
||||
@ -58,7 +39,7 @@ func (s *WorkspaceService) GetWorkspaceProfile(ctx context.Context, _ *apiv2pb.G
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WorkspaceService) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.GetWorkspaceSettingRequest) (*apiv2pb.GetWorkspaceSettingResponse, error) {
|
||||
func (s *APIV2Service) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.GetWorkspaceSettingRequest) (*apiv2pb.GetWorkspaceSettingResponse, error) {
|
||||
isAdmin := false
|
||||
userID, ok := ctx.Value(userIDContextKey).(int32)
|
||||
if ok {
|
||||
@ -74,7 +55,9 @@ func (s *WorkspaceService) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.G
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list workspace settings: %v", err)
|
||||
}
|
||||
workspaceSetting := &apiv2pb.WorkspaceSetting{}
|
||||
workspaceSetting := &apiv2pb.WorkspaceSetting{
|
||||
EnableSignup: true,
|
||||
}
|
||||
for _, v := range workspaceSettings {
|
||||
if v.Key == storepb.WorkspaceSettingKey_WORKSAPCE_SETTING_ENABLE_SIGNUP {
|
||||
workspaceSetting.EnableSignup = v.GetEnableSignup()
|
||||
@ -86,8 +69,6 @@ func (s *WorkspaceService) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.G
|
||||
// For some settings, only admin can get the value.
|
||||
if v.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_LICENSE_KEY {
|
||||
workspaceSetting.LicenseKey = v.GetLicenseKey()
|
||||
} else if v.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_RESOURCE_RELATIVE_PATH {
|
||||
workspaceSetting.ResourceRelativePath = v.GetResourceRelativePath()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,12 +77,12 @@ func (s *WorkspaceService) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.G
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WorkspaceService) UpdateWorkspaceSetting(ctx context.Context, request *apiv2pb.UpdateWorkspaceSettingRequest) (*apiv2pb.UpdateWorkspaceSettingResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask) == 0 {
|
||||
func (s *APIV2Service) UpdateWorkspaceSetting(ctx context.Context, request *apiv2pb.UpdateWorkspaceSettingRequest) (*apiv2pb.UpdateWorkspaceSettingResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
|
||||
}
|
||||
|
||||
for _, path := range request.UpdateMask {
|
||||
for _, path := range request.UpdateMask.Paths {
|
||||
if path == "license_key" {
|
||||
if _, err := s.Store.UpsertWorkspaceSetting(ctx, &storepb.WorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_LICENSE_KEY,
|
||||
@ -120,20 +101,7 @@ func (s *WorkspaceService) UpdateWorkspaceSetting(ctx context.Context, request *
|
||||
}); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to update workspace setting: %v", err)
|
||||
}
|
||||
} else if path == "resource_relative_path" {
|
||||
if _, err := s.Store.UpsertWorkspaceSetting(ctx, &storepb.WorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_RESOURCE_RELATIVE_PATH,
|
||||
Value: &storepb.WorkspaceSetting_ResourceRelativePath{
|
||||
ResourceRelativePath: request.Setting.ResourceRelativePath,
|
||||
},
|
||||
}); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to update workspace setting: %v", err)
|
||||
}
|
||||
} else if path == "custom_style" {
|
||||
if !s.LicenseService.IsFeatureEnabled(license.FeatureTypeCustomeStyle) {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "feature custom style is not available")
|
||||
}
|
||||
|
||||
if _, err := s.Store.UpsertWorkspaceSetting(ctx, &storepb.WorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_CUSTOM_STYLE,
|
||||
Value: &storepb.WorkspaceSetting_CustomStyle{
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/boojack/slash/internal/log"
|
||||
"github.com/boojack/slash/server"
|
||||
"github.com/boojack/slash/server/metric"
|
||||
"github.com/boojack/slash/server/profile"
|
||||
"github.com/boojack/slash/store"
|
||||
"github.com/boojack/slash/store/db"
|
||||
@ -50,6 +51,9 @@ var (
|
||||
return
|
||||
}
|
||||
|
||||
// nolint
|
||||
metric.NewMetricClient(s.Secret, *serverProfile)
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
// Trigger graceful shutdown on SIGINT or SIGTERM.
|
||||
// The default signal sent by the `kill` command is SIGTERM,
|
43
docs/getting-started/collections.md
Normal file
43
docs/getting-started/collections.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Slash Collections
|
||||
|
||||
**Slash Collections** introduces a feature to help you better organize and manage related Shortcuts within the Slash Shortcuts platform.
|
||||
|
||||
## What is a Collection?
|
||||
|
||||
A Collection is like a virtual folder where you can group and organize your related Shortcuts. It acts as a container that holds Shortcuts together for a specific purpose or theme. Let's break down the key attributes:
|
||||
|
||||
- **Name:** Your chosen label for the Collection. This becomes a crucial part of the URL, enabling direct and quick access to the Collection. For example, if your Collection is named "work-projects", the direct access link would be `c/work-projects`. This user-defined name significantly enhances the accessibility and recognition of your Collections.
|
||||
- **Title:** A brief title summarizing the Collection's content.
|
||||
- **Description:** A short description explaining what the Collection is about.
|
||||
- **Shortcuts:** The Shortcuts included in the Collection.
|
||||
- **Visibility:** Settings to control who can access the Collection.
|
||||
|
||||
## What Problems Does It Solve?
|
||||
|
||||
Slash Collections tackle the challenge of efficiently managing and organizing related Shortcuts. By grouping Shortcuts into Collections, you can create a more structured and accessible workflow. This makes it easier to find, access, and share information based on specific themes or projects.
|
||||
|
||||
## How to Use Collections
|
||||
|
||||
### Creating a Collection
|
||||
|
||||
1. **Define the Collection:** Give your Collection a meaningful name and a descriptive title.
|
||||
2. **Add Details:** Provide a brief description of the content within the Collection.
|
||||
3. **Add Shortcuts:** Include relevant Shortcuts by selecting them from your existing list.
|
||||
4. **Set Visibility:** Choose who should have access to the Collection.
|
||||
5. **Save:** Once saved, your Collection is ready to use.
|
||||
|
||||
### Accessing Collections
|
||||
|
||||
Access a Collection directly by using the assigned name. For example, if your Collection is named "work-projects", the direct access link would be `{YOUR_DOMAIN}/c/work-projects`.
|
||||
|
||||
### Updating and Managing Collections
|
||||
|
||||
Modify Collection details, such as name, title, or included Shortcuts, to keep your organization streamlined and relevant.
|
||||
|
||||
### Sharing Collections
|
||||
|
||||
Share Collections by providing the assigned name to collaborators for easy access to grouped Shortcuts.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Slash Collections offer a user-friendly and organized way to group, manage, and share related Shortcuts. By utilizing the defined Collection attributes, users can seamlessly categorize and access information, promoting collaboration and improving overall productivity.
|
47
docs/getting-started/shortcuts.md
Normal file
47
docs/getting-started/shortcuts.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Slash Shortcuts
|
||||
|
||||
**Slash Shortcuts** is a handy tool designed to make handling and sharing links in your digital workspace a breeze.
|
||||
|
||||
## What is a Shortcut?
|
||||
|
||||
A Shortcut is a simplified version of a link with essential details, making it easy to remember, organize, and share. Let's break down the key elements:
|
||||
|
||||
- **Name:** Your chosen label for the Shortcut. This becomes a crucial part of the URL, enabling direct and quick access to the Shortcut. For example, if your Shortcut is named "meet-john", the direct access link would be `s/meet-john`. This user-defined name significantly enhances the accessibility and recognition of your Shortcuts.
|
||||
- **Link:** The original web link you want to streamline.
|
||||
- **Title:** A quick overview of what's behind the link.
|
||||
- **Tags:** Custom labels for easy sorting.
|
||||
- **Description:** A short summary of the content.
|
||||
- **Visibility:** Controls who can access the Shortcut.
|
||||
|
||||
## How to Use Shortcuts
|
||||
|
||||
### Creating a Shortcut
|
||||
|
||||
1. **Define the Link:** Paste the original link you want to simplify.
|
||||
2. **Add Details:** Give it a name, tags, and a brief description for better organization.
|
||||
3. **Set Visibility:** Choose who should be able to access the Shortcut.
|
||||
4. **Save:** Once saved, your Shortcut is ready to go.
|
||||
|
||||
### Accessing Shortcuts
|
||||
|
||||
#### Direct Access
|
||||
|
||||
Effortlessly access your Shortcut's content directly by using the assigned name as part of the Slash Shortcuts format.
|
||||
|
||||
For example, if your Shortcut is named "meet-john", the direct access link would be `{YOUR_DOMAIN}/s/meet-john`. Simply enter this user-friendly shortcut into your browser to reach the associated content with ease.
|
||||
|
||||
#### Browser Extension Access
|
||||
|
||||
Install the Slash Shortcuts browser extension for even quicker access. Once installed, simply type `s/meet-john` into your browser's address bar, and the extension will seamlessly redirect you to the corresponding page.
|
||||
|
||||
### Updating and Managing Shortcuts
|
||||
|
||||
Adjust attributes like name and tags to update a Shortcut. Keep your Shortcuts organized based on categories and visibility settings.
|
||||
|
||||
### Sharing Shortcuts
|
||||
|
||||
Share Shortcuts by providing the assigned name to collaborators for easy access.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Shortcuts provide a simple way to manage, organize, and share links within your digital workspace. By using the defined Shortcut attributes, users can easily create, access, and share information, promoting collaboration and boosting productivity.
|
@ -8,7 +8,7 @@ Slash provides a browser extension to help you use your shortcuts in the search
|
||||
|
||||
For Chromuim based browsers, you can install the extension from the [Chrome Web Store](https://chrome.google.com/webstore/detail/slash/ebaiehmkammnacjadffpicipfckgeobg).
|
||||
|
||||
For Firefox, we don't support the Firefox Add-ons platform yet. And we are working on it.
|
||||
For Firefox, you can install the extension from the [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/your-slash/).
|
||||
|
||||
### Generate an access token
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 257 KiB |
@ -1,58 +1,64 @@
|
||||
{
|
||||
"name": "slash-extension",
|
||||
"displayName": "Slash",
|
||||
"version": "0.1.4",
|
||||
"version": "1.0.1",
|
||||
"description": "An open source, self-hosted bookmarks and link sharing platform. Save and share your links very easily.",
|
||||
"scripts": {
|
||||
"dev": "plasmo dev",
|
||||
"build": "plasmo build",
|
||||
"package": "plasmo package",
|
||||
"lint": "eslint --ext .js,.ts,.tsx, src",
|
||||
"lint-fix": "eslint --ext .js,.ts,.tsx, src --fix"
|
||||
"lint-fix": "eslint --ext .js,.ts,.tsx, src --fix",
|
||||
"type-gen": "cd ../../proto && buf generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/joy": "5.0.0-beta.0",
|
||||
"@plasmohq/storage": "^1.8.0",
|
||||
"axios": "^1.5.0",
|
||||
"@mui/joy": "5.0.0-beta.14",
|
||||
"@plasmohq/storage": "^1.8.1",
|
||||
"axios": "^1.6.1",
|
||||
"classnames": "^2.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.264.0",
|
||||
"plasmo": "0.82.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"plasmo": "^0.83.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"zustand": "^4.4.1"
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "4.1.0",
|
||||
"@types/chrome": "0.0.241",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/node": "20.4.2",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.49.0",
|
||||
"@bufbuild/buf": "^1.27.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.1",
|
||||
"@types/chrome": "^0.0.241",
|
||||
"@types/lodash-es": "^4.17.11",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"long": "^5.2.3",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "2.6.2",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.8",
|
||||
"protobufjs": "^7.2.5",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "5.1.6"
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"manifest": {
|
||||
"omnibox": {
|
||||
"keyword": "s"
|
||||
"keyword": "s/"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage"
|
||||
"activeTab",
|
||||
"storage",
|
||||
"webRequest"
|
||||
],
|
||||
"host_permissions": [
|
||||
"*://*/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
1799
frontend/extension/pnpm-lock.yaml
generated
1799
frontend/extension/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,32 +1,43 @@
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import { Storage } from "@plasmohq/storage";
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
|
||||
const storage = new Storage();
|
||||
const urlRegex = /https?:\/\/s\/(.+)/;
|
||||
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
|
||||
if (!tab.url) {
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
(param) => {
|
||||
(async () => {
|
||||
if (!param.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shortcutName = getShortcutNameFromUrl(tab.url);
|
||||
const shortcutName = getShortcutNameFromUrl(param.url);
|
||||
if (shortcutName) {
|
||||
const shortcuts = (await storage.getItem<Shortcut[]>("shortcuts")) || [];
|
||||
const shortcut = shortcuts.find((shortcut) => shortcut.name === shortcutName);
|
||||
if (!shortcut) {
|
||||
return;
|
||||
}
|
||||
return chrome.tabs.update(tabId, { url: shortcut.link });
|
||||
return chrome.tabs.update({ url: shortcut.link });
|
||||
}
|
||||
});
|
||||
})();
|
||||
},
|
||||
{ urls: ["*://s/*", "*://*/search*"] }
|
||||
);
|
||||
|
||||
chrome.omnibox.onInputEntered.addListener(async (text) => {
|
||||
chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
|
||||
const shortcuts = (await storage.getItem<Shortcut[]>("shortcuts")) || [];
|
||||
const shortcut = shortcuts.find((shortcut) => shortcut.name === text);
|
||||
if (!shortcut) {
|
||||
return;
|
||||
}
|
||||
return chrome.tabs.update({ url: shortcut.link });
|
||||
if (disposition === "currentTab") {
|
||||
chrome.tabs.update({ url: shortcut.link });
|
||||
} else if (disposition === "newForegroundTab") {
|
||||
chrome.tabs.create({ url: shortcut.link });
|
||||
} else if (disposition === "newBackgroundTab") {
|
||||
chrome.tabs.create({ url: shortcut.link, active: false });
|
||||
}
|
||||
});
|
||||
|
||||
const getShortcutNameFromUrl = (urlString: string) => {
|
||||
|
@ -3,7 +3,8 @@ import { useStorage } from "@plasmohq/storage/hook";
|
||||
import axios from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { CreateShortcutResponse, OpenGraphMetadata, Visibility } from "@/types/proto/api/v2/shortcut_service";
|
||||
import { Visibility } from "@/types/proto/api/v2/common";
|
||||
import { CreateShortcutResponse, OpenGraphMetadata } from "@/types/proto/api/v2/shortcut_service";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const generateTempName = (length = 6) => {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import { useStorage } from "@plasmohq/storage/hook";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import useFaviconStore from "../stores/favicon";
|
||||
import { getFaviconWithGoogleS2 } from "@/helpers/utils";
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface Props {
|
||||
@ -11,17 +10,8 @@ interface Props {
|
||||
|
||||
const ShortcutView = (props: Props) => {
|
||||
const { shortcut } = props;
|
||||
const faviconStore = useFaviconStore();
|
||||
const [domain] = useStorage<string>("domain", "");
|
||||
const [favicon, setFavicon] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => {
|
||||
if (url) {
|
||||
setFavicon(url);
|
||||
}
|
||||
});
|
||||
}, [shortcut.link]);
|
||||
const favicon = getFaviconWithGoogleS2(shortcut.link);
|
||||
|
||||
const handleShortcutLinkClick = () => {
|
||||
const shortcutLink = `${domain}/s/${shortcut.name}`;
|
||||
@ -32,13 +22,13 @@ const ShortcutView = (props: Props) => {
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
"group w-full px-3 py-2 flex flex-col justify-start items-start border rounded-lg hover:bg-gray-100 hover:shadow"
|
||||
"group w-full px-3 py-2 flex flex-col justify-start items-start border rounded-lg hover:bg-gray-100 hover:shadow dark:border-zinc-800 dark:hover:bg-zinc-800"
|
||||
)}
|
||||
>
|
||||
<div className="w-full flex flex-row justify-start items-center">
|
||||
<span className={classNames("w-5 h-5 flex justify-center items-center overflow-clip shrink-0")}>
|
||||
{favicon ? (
|
||||
<img className="w-full h-auto rounded-full" src={favicon} decoding="async" loading="lazy" />
|
||||
<img className="w-full h-auto rounded-lg" src={favicon} decoding="async" loading="lazy" />
|
||||
) : (
|
||||
<Icon.CircleSlash className="w-full h-auto text-gray-400" />
|
||||
)}
|
||||
@ -52,13 +42,13 @@ const ShortcutView = (props: Props) => {
|
||||
onClick={handleShortcutLinkClick}
|
||||
>
|
||||
<div className="truncate">
|
||||
<span>{shortcut.title}</span>
|
||||
<span className="dark:text-gray-400">{shortcut.title}</span>
|
||||
{shortcut.title ? (
|
||||
<span className="text-gray-400">(s/{shortcut.name})</span>
|
||||
<span className="text-gray-500">(s/{shortcut.name})</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gray-400">s/</span>
|
||||
<span className="truncate">{shortcut.name}</span>
|
||||
<span className="text-gray-400 dark:text-gray-500">s/</span>
|
||||
<span className="truncate dark:text-gray-400">{shortcut.name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import { useStorage } from "@plasmohq/storage/hook";
|
||||
import classNames from "classnames";
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import ShortcutView from "./ShortcutView";
|
||||
|
||||
const ShortcutsContainer = () => {
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { Storage } from "@plasmohq/storage";
|
||||
import axios from "axios";
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
export const getUrlFavicon = async (url: string) => {
|
||||
const domain = await storage.getItem<string>("domain");
|
||||
const accessToken = await storage.getItem<string>("access_token");
|
||||
return axios.get<string>(`${domain}/api/v1/url/favicon?url=${url}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
};
|
@ -3,3 +3,12 @@ import { isNull, isUndefined } from "lodash-es";
|
||||
export const isNullorUndefined = (value: any) => {
|
||||
return isNull(value) || isUndefined(value);
|
||||
};
|
||||
|
||||
export const getFaviconWithGoogleS2 = (url: string) => {
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
return `https://www.google.com/s2/favicons?sz=128&domain=${urlObject.hostname}`;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
43
frontend/extension/src/hooks/useColorTheme.ts
Normal file
43
frontend/extension/src/hooks/useColorTheme.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useColorScheme } from "@mui/joy";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useColorTheme = () => {
|
||||
const { mode: colorTheme, setMode: setColorTheme } = useColorScheme();
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (colorTheme === "light") {
|
||||
root.classList.remove("dark");
|
||||
} else if (colorTheme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
if (darkMediaQuery.matches) {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
|
||||
const handleColorSchemeChange = (e: MediaQueryListEvent) => {
|
||||
if (e.matches) {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
};
|
||||
try {
|
||||
darkMediaQuery.addEventListener("change", handleColorSchemeChange);
|
||||
} catch (error) {
|
||||
console.error("failed to initial color scheme listener", error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
darkMediaQuery.removeEventListener("change", handleColorSchemeChange);
|
||||
};
|
||||
}
|
||||
}, [colorTheme]);
|
||||
|
||||
return { colorTheme, setColorTheme };
|
||||
};
|
||||
|
||||
export default useColorTheme;
|
@ -1,12 +1,13 @@
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import { Button, Divider, Input } from "@mui/joy";
|
||||
import { Button, CssVarsProvider, Divider, Input, Select, Option } from "@mui/joy";
|
||||
import { useStorage } from "@plasmohq/storage/hook";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Toaster, toast } from "react-hot-toast";
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import Icon from "./components/Icon";
|
||||
import Logo from "./components/Logo";
|
||||
import PullShortcutsButton from "./components/PullShortcutsButton";
|
||||
import ShortcutsContainer from "./components/ShortcutsContainer";
|
||||
import useColorTheme from "./hooks/useColorTheme";
|
||||
import "./style.css";
|
||||
|
||||
interface SettingState {
|
||||
@ -14,7 +15,23 @@ interface SettingState {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
const colorThemeOptions = [
|
||||
{
|
||||
value: "system",
|
||||
label: "System",
|
||||
},
|
||||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
},
|
||||
];
|
||||
|
||||
const IndexOptions = () => {
|
||||
const { colorTheme, setColorTheme } = useColorTheme();
|
||||
const [domain, setDomain] = useStorage<string>("domain", (v) => (v ? v : ""));
|
||||
const [accessToken, setAccessToken] = useStorage<string>("access_token", (v) => (v ? v : ""));
|
||||
const [settingState, setSettingState] = useState<SettingState>({
|
||||
@ -44,12 +61,15 @@ const IndexOptions = () => {
|
||||
toast.success("Setting saved");
|
||||
};
|
||||
|
||||
const handleSelectColorTheme = async (colorTheme: string) => {
|
||||
setColorTheme(colorTheme as any);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full">
|
||||
<div className="w-full flex flex-row justify-center items-center">
|
||||
<a
|
||||
className="bg-yellow-100 mt-12 py-2 px-3 rounded-full border flex flex-row justify-start items-center cursor-pointer shadow hover:underline hover:text-blue-600"
|
||||
className="bg-yellow-100 dark:bg-yellow-500 dark:opacity-70 mt-12 py-2 px-3 rounded-full border dark:border-yellow-600 flex flex-row justify-start items-center cursor-pointer shadow hover:underline hover:text-blue-600"
|
||||
href="https://github.com/boojack/slash#browser-extension"
|
||||
target="_blank"
|
||||
>
|
||||
@ -60,7 +80,7 @@ const IndexOptions = () => {
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-lg mx-auto flex flex-col justify-start items-start mt-12">
|
||||
<h2 className="flex flex-row justify-start items-center mb-6 text-2xl">
|
||||
<h2 className="flex flex-row justify-start items-center mb-6 text-2xl dark:text-gray-400">
|
||||
<Logo className="w-10 h-auto mr-2" />
|
||||
<span>Slash</span>
|
||||
<span className="mx-2 text-gray-400">/</span>
|
||||
@ -70,10 +90,10 @@ const IndexOptions = () => {
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-4">
|
||||
<div className="mb-2 text-base w-full flex flex-row justify-between items-center">
|
||||
<span>Domain</span>
|
||||
<span className="dark:text-gray-400">Domain</span>
|
||||
{domain !== "" && (
|
||||
<a
|
||||
className="text-sm flex flex-row justify-start items-center hover:underline hover:text-blue-600"
|
||||
className="text-sm flex flex-row justify-start items-center dark:text-gray-400 hover:underline hover:text-blue-600"
|
||||
href={domain}
|
||||
target="_blank"
|
||||
>
|
||||
@ -94,7 +114,7 @@ const IndexOptions = () => {
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<span className="mb-2 text-base">Access Token</span>
|
||||
<span className="mb-2 text-base dark:text-gray-400">Access Token</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
@ -106,9 +126,28 @@ const IndexOptions = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full mt-6">
|
||||
<div className="w-full mt-6 flex flex-row justify-end">
|
||||
<Button onClick={handleSaveSetting}>Save</Button>
|
||||
</div>
|
||||
|
||||
<Divider className="!my-6" />
|
||||
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">Preference</p>
|
||||
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row justify-start items-center gap-x-1">
|
||||
<span className="dark:text-gray-400">Color Theme</span>
|
||||
</div>
|
||||
<Select defaultValue={colorTheme} onChange={(_, value) => handleSelectColorTheme(value)}>
|
||||
{colorThemeOptions.map((option) => {
|
||||
return (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isInitialized && (
|
||||
@ -116,7 +155,7 @@ const IndexOptions = () => {
|
||||
<Divider className="!my-6" />
|
||||
|
||||
<h2 className="flex flex-row justify-start items-center mb-4">
|
||||
<span className="text-lg">Shortcuts</span>
|
||||
<span className="text-lg dark:text-gray-400">Shortcuts</span>
|
||||
<span className="text-gray-500 mr-1">({shortcuts.length})</span>
|
||||
<PullShortcutsButton />
|
||||
</h2>
|
||||
@ -125,10 +164,16 @@ const IndexOptions = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Toaster position="top-center" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexOptions;
|
||||
const Options = () => {
|
||||
return (
|
||||
<CssVarsProvider>
|
||||
<IndexOptions />
|
||||
<Toaster position="top-right" />
|
||||
</CssVarsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Options;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import { Button, Divider, IconButton } from "@mui/joy";
|
||||
import { Button, CssVarsProvider, Divider, IconButton } from "@mui/joy";
|
||||
import { useStorage } from "@plasmohq/storage/hook";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import CreateShortcutsButton from "@/components/CreateShortcutsButton";
|
||||
@ -7,9 +6,12 @@ import Icon from "@/components/Icon";
|
||||
import Logo from "@/components/Logo";
|
||||
import PullShortcutsButton from "@/components/PullShortcutsButton";
|
||||
import ShortcutsContainer from "@/components/ShortcutsContainer";
|
||||
import type { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import useColorTheme from "./hooks/useColorTheme";
|
||||
import "./style.css";
|
||||
|
||||
const IndexPopup = () => {
|
||||
useColorTheme();
|
||||
const [domain] = useStorage<string>("domain", "");
|
||||
const [accessToken] = useStorage<string>("access_token", "");
|
||||
const [shortcuts] = useStorage<Shortcut[]>("shortcuts", []);
|
||||
@ -25,10 +27,9 @@ const IndexPopup = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full min-w-[512px] px-4 pt-4">
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row justify-start items-center">
|
||||
<div className="flex flex-row justify-start items-center dark:text-gray-400">
|
||||
<Logo className="w-6 h-auto mr-2" />
|
||||
<span className="">Slash</span>
|
||||
{isInitialized && (
|
||||
@ -59,22 +60,15 @@ const IndexPopup = () => {
|
||||
<div className="w-full flex flex-row justify-between items-center mb-2">
|
||||
<div className="flex flex-row justify-start items-center">
|
||||
<IconButton size="sm" variant="plain" color="neutral" onClick={handleSettingButtonClick}>
|
||||
<Icon.Settings className="w-5 h-auto text-gray-500" />
|
||||
<Icon.Settings className="w-5 h-auto text-gray-500 dark:text-gray-400" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="plain"
|
||||
color="neutral"
|
||||
component="a"
|
||||
href="https://github.com/boojack/slash"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon.Github className="w-5 h-auto text-gray-500" />
|
||||
<IconButton size="sm" variant="plain" color="neutral" component="a" href="https://github.com/boojack/slash" target="_blank">
|
||||
<Icon.Github className="w-5 h-auto text-gray-500 dark:text-gray-400" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center">
|
||||
<a
|
||||
className="text-sm flex flex-row justify-start items-center text-gray-500 hover:underline hover:text-blue-600"
|
||||
className="text-sm flex flex-row justify-start items-center text-gray-500 dark:text-gray-400 hover:underline hover:text-blue-600"
|
||||
href={domain}
|
||||
target="_blank"
|
||||
>
|
||||
@ -86,12 +80,13 @@ const IndexPopup = () => {
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full flex flex-col justify-start items-center">
|
||||
<p>No domain and access token found.</p>
|
||||
<Icon.Cookie strokeWidth={1} className="w-20 h-auto mb-4 text-gray-400" />
|
||||
<p className="dark:text-gray-400">Please set your domain and access token first.</p>
|
||||
<div className="w-full flex flex-row justify-center items-center py-4">
|
||||
<Button size="sm" color="primary" onClick={handleSettingButtonClick}>
|
||||
<Icon.Settings className="w-5 h-auto mr-1" /> Setting
|
||||
</Button>
|
||||
<span className="mx-2">Or</span>
|
||||
<span className="mx-2 dark:text-gray-400">Or</span>
|
||||
<Button size="sm" variant="outlined" color="neutral" onClick={handleRefreshButtonClick}>
|
||||
<Icon.RefreshCcw className="w-5 h-auto mr-1" /> Refresh
|
||||
</Button>
|
||||
@ -100,10 +95,16 @@ const IndexPopup = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Toaster position="top-right" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexPopup;
|
||||
const Popup = () => {
|
||||
return (
|
||||
<CssVarsProvider>
|
||||
<IndexPopup />
|
||||
<Toaster position="top-right" />
|
||||
</CssVarsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
|
@ -1,41 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { getUrlFavicon } from "../helpers/api";
|
||||
|
||||
interface FaviconState {
|
||||
cache: {
|
||||
[key: string]: string;
|
||||
};
|
||||
getOrFetchUrlFavicon: (url: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const useFaviconStore = create<FaviconState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
cache: {},
|
||||
getOrFetchUrlFavicon: async (url: string) => {
|
||||
const cache = get().cache;
|
||||
if (cache[url]) {
|
||||
return cache[url];
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: favicon } = await getUrlFavicon(url);
|
||||
if (favicon) {
|
||||
cache[url] = favicon;
|
||||
set(cache);
|
||||
return favicon;
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
return "";
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "favicon_cache",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useFaviconStore;
|
@ -5,7 +5,7 @@
|
||||
body,
|
||||
html,
|
||||
#root {
|
||||
@apply text-base;
|
||||
@apply text-base dark:bg-zinc-900;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans", "Noto Sans CJK SC", "Microsoft YaHei UI", "Microsoft YaHei",
|
||||
"WenQuanYi Micro Hei", sans-serif, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
"Noto Color Emoji";
|
||||
|
@ -11,7 +11,14 @@
|
||||
"language": "Language",
|
||||
"search": "Search",
|
||||
"email": "Email",
|
||||
"password": "Password"
|
||||
"password": "Password",
|
||||
"account": "Account"
|
||||
},
|
||||
"auth": {
|
||||
"sign-in": "Sign in",
|
||||
"sign-up": "Sign up",
|
||||
"sign-out": "Sign out",
|
||||
"create-your-account": "Create your account"
|
||||
},
|
||||
"analytics": {
|
||||
"self": "Analytics",
|
||||
@ -24,6 +31,7 @@
|
||||
"operating-system": "Operating System"
|
||||
},
|
||||
"shortcut": {
|
||||
"visits": "{{count}} visits",
|
||||
"visibility": {
|
||||
"private": {
|
||||
"self": "Private",
|
||||
@ -38,5 +46,37 @@
|
||||
"description": "Visible to everyone on the internet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"mine": "Mine",
|
||||
"compact-mode": "Compact mode",
|
||||
"order-by": "Order by",
|
||||
"direction": "Direction"
|
||||
},
|
||||
"user": {
|
||||
"self": "User",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"role": "Role",
|
||||
"profile": "Profile",
|
||||
"action": {
|
||||
"add-user": "Add user"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"self": "Setting",
|
||||
"preference": {
|
||||
"self": "Preference",
|
||||
"color-theme": "Color theme"
|
||||
},
|
||||
"workspace": {
|
||||
"self": "Workspace settings",
|
||||
"custom-style": "Custom style",
|
||||
"enable-user-signup": {
|
||||
"self": "Enable user signup",
|
||||
"description": "Once enabled, other users can signup."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,14 @@
|
||||
"language": "语言",
|
||||
"search": "搜索",
|
||||
"email": "邮箱",
|
||||
"password": "密码"
|
||||
"password": "密码",
|
||||
"account": "账号"
|
||||
},
|
||||
"auth": {
|
||||
"sign-in": "登录",
|
||||
"sign-up": "注册",
|
||||
"sign-out": "退出登录",
|
||||
"create-your-account": "创建账号"
|
||||
},
|
||||
"analytics": {
|
||||
"self": "分析",
|
||||
@ -24,6 +31,7 @@
|
||||
"operating-system": "操作系统"
|
||||
},
|
||||
"shortcut": {
|
||||
"visits": "{{count}} 次访问",
|
||||
"visibility": {
|
||||
"private": {
|
||||
"self": "私有的",
|
||||
@ -38,5 +46,37 @@
|
||||
"description": "对任何人可见"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"all": "所有",
|
||||
"mine": "我的",
|
||||
"compact-mode": "紧凑模式",
|
||||
"order-by": "排序方式",
|
||||
"direction": "方向"
|
||||
},
|
||||
"user": {
|
||||
"self": "用户",
|
||||
"nickname": "昵称",
|
||||
"email": "邮箱",
|
||||
"role": "角色",
|
||||
"profile": "账号",
|
||||
"action": {
|
||||
"add-user": "添加用户"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"self": "设置",
|
||||
"preference": {
|
||||
"self": "偏好设置",
|
||||
"color-theme": "主题"
|
||||
},
|
||||
"workspace": {
|
||||
"self": "系统设置",
|
||||
"custom-style": "自定义样式",
|
||||
"enable-user-signup": {
|
||||
"self": "启用用户注册",
|
||||
"description": "允许其他用户注册新账号"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,50 +5,52 @@
|
||||
"build": "tsc && vite build",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint --ext .js,.ts,.tsx, src",
|
||||
"lint-fix": "eslint --ext .js,.ts,.tsx, src --fix"
|
||||
"lint-fix": "eslint --ext .js,.ts,.tsx, src --fix",
|
||||
"type-gen": "cd ../../proto && buf generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/joy": "5.0.0-beta.7",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"axios": "^0.27.2",
|
||||
"@mui/joy": "5.0.0-beta.14",
|
||||
"@reduxjs/toolkit": "^1.9.7",
|
||||
"axios": "^1.6.0",
|
||||
"classnames": "^2.3.2",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"i18next": "^23.5.1",
|
||||
"i18next": "^23.6.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.263.1",
|
||||
"nice-grpc-web": "^3.3.1",
|
||||
"lucide-react": "^0.292.0",
|
||||
"nice-grpc-web": "^3.3.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^13.2.2",
|
||||
"react-redux": "^8.1.2",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-i18next": "^13.3.1",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"react-use": "^17.4.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"zustand": "^4.4.1"
|
||||
"tailwindcss": "^3.3.5",
|
||||
"zustand": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/react": "^18.2.22",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
"@typescript-eslint/parser": "^6.7.2",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"@bufbuild/buf": "^1.27.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.1",
|
||||
"@types/lodash-es": "^4.17.11",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@vitejs/plugin-react-swc": "^3.4.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"long": "^5.2.3",
|
||||
"postcss": "^8.4.30",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "2.6.2",
|
||||
"protobufjs": "^7.2.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9"
|
||||
"vite": "^4.5.0"
|
||||
}
|
||||
}
|
||||
|
1164
frontend/web/pnpm-lock.yaml
generated
1164
frontend/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 257 KiB |
@ -34,6 +34,12 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
|
||||
<span className="py-2 pr-2 text-right font-semibold text-sm text-gray-500">{t("analytics.visitors")}</span>
|
||||
</div>
|
||||
<div className="w-full divide-y divide-gray-200 dark:divide-zinc-800">
|
||||
{analytics.referenceData.length === 0 && (
|
||||
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
|
||||
<Icon.PackageOpen className="w-6 h-auto" />
|
||||
<p className="ml-2">No data found.</p>
|
||||
</div>
|
||||
)}
|
||||
{analytics.referenceData.map((reference) => (
|
||||
<div key={reference.name} className="w-full flex flex-row justify-between items-center">
|
||||
<span className="whitespace-nowrap py-2 px-2 text-sm truncate text-gray-900 dark:text-gray-500">
|
||||
@ -89,6 +95,12 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
|
||||
<span className="py-2 pr-2 text-right text-sm font-semibold text-gray-500">{t("analytics.visitors")}</span>
|
||||
</div>
|
||||
<div className="w-full divide-y divide-gray-200 dark:divide-zinc-800">
|
||||
{analytics.browserData.length === 0 && (
|
||||
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
|
||||
<Icon.PackageOpen className="w-6 h-auto" />
|
||||
<p className="ml-2">No data found.</p>
|
||||
</div>
|
||||
)}
|
||||
{analytics.browserData.map((reference) => (
|
||||
<div key={reference.name} className="w-full flex flex-row justify-between items-center">
|
||||
<span className="whitespace-nowrap py-2 px-2 text-sm text-gray-900 truncate dark:text-gray-500">
|
||||
@ -106,6 +118,12 @@ const AnalyticsView: React.FC<Props> = (props: Props) => {
|
||||
<span className="py-2 pr-2 text-right text-sm font-semibold text-gray-500">{t("analytics.visitors")}</span>
|
||||
</div>
|
||||
<div className="w-full divide-y divide-gray-200">
|
||||
{analytics.deviceData.length === 0 && (
|
||||
<div className="w-full flex flex-row justify-center items-center py-6 text-gray-400">
|
||||
<Icon.PackageOpen className="w-6 h-auto" />
|
||||
<p className="ml-2">No data found.</p>
|
||||
</div>
|
||||
)}
|
||||
{analytics.deviceData.map((device) => (
|
||||
<div key={device.name} className="w-full flex flex-row justify-between items-center">
|
||||
<span className="whitespace-nowrap py-2 px-2 text-sm text-gray-900 truncate">{device.name || "Unknown"}</span>
|
||||
|
119
frontend/web/src/components/CollectionView.tsx
Normal file
119
frontend/web/src/components/CollectionView.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import classNames from "classnames";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { absolutifyLink } from "@/helpers/utils";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||
import { useAppSelector } from "@/stores";
|
||||
import useCollectionStore from "@/stores/v1/collection";
|
||||
import { Collection } from "@/types/proto/api/v2/collection_service";
|
||||
import { showCommonDialog } from "./Alert";
|
||||
import CreateCollectionDialog from "./CreateCollectionDialog";
|
||||
import Icon from "./Icon";
|
||||
import ShortcutView from "./ShortcutView";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
|
||||
interface Props {
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
const CollectionView = (props: Props) => {
|
||||
const { collection } = props;
|
||||
const { t } = useTranslation();
|
||||
const { sm } = useResponsiveWidth();
|
||||
const navigateTo = useNavigateTo();
|
||||
const collectionStore = useCollectionStore();
|
||||
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
||||
const [showEditDialog, setShowEditDialog] = useState<boolean>(false);
|
||||
const shortcuts = collection.shortcutIds
|
||||
.map((shortcutId) => shortcutList.find((shortcut) => shortcut?.id === shortcutId))
|
||||
.filter(Boolean) as any as Shortcut[];
|
||||
|
||||
const handleCopyCollectionLink = () => {
|
||||
copy(absolutifyLink(`/c/${collection.name}`));
|
||||
toast.success("Collection link copied to clipboard.");
|
||||
};
|
||||
|
||||
const handleDeleteCollectionButtonClick = () => {
|
||||
showCommonDialog({
|
||||
title: "Delete Collection",
|
||||
content: `Are you sure to delete collection \`${collection.name}\`? You cannot undo this action.`,
|
||||
style: "danger",
|
||||
onConfirm: async () => {
|
||||
await collectionStore.deleteCollection(collection.id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleShortcutClick = (shortcut: Shortcut) => {
|
||||
navigateTo(`/shortcut/${shortcut.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames("w-full flex flex-col justify-start items-start border rounded-lg hover:shadow dark:border-zinc-800")}>
|
||||
<div className="bg-gray-100 dark:bg-zinc-800 px-3 py-2 w-full flex flex-row justify-between items-center rounded-t-lg">
|
||||
<div className="w-auto flex flex-col justify-start items-start mr-2">
|
||||
<div className="w-full truncate" onClick={handleCopyCollectionLink}>
|
||||
<span className="leading-6 font-medium dark:text-gray-400">{collection.title}</span>
|
||||
<span className="ml-1 leading-6 text-gray-500 dark:text-gray-400">(c/{collection.name})</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{collection.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center shrink-0">
|
||||
<Link className="w-full text-gray-400 cursor-pointer hover:text-gray-500" to={`/c/${collection.name}`}>
|
||||
<Icon.Share className="w-4 h-auto mr-2" />
|
||||
</Link>
|
||||
<Dropdown
|
||||
actionsClassName="!w-28 dark:text-gray-500"
|
||||
actions={
|
||||
<>
|
||||
<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 dark:hover:bg-zinc-800"
|
||||
onClick={() => setShowEditDialog(true)}
|
||||
>
|
||||
<Icon.Edit className="w-4 h-auto mr-2" /> {t("common.edit")}
|
||||
</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 dark:hover:bg-zinc-800"
|
||||
onClick={() => {
|
||||
handleDeleteCollectionButtonClick();
|
||||
}}
|
||||
>
|
||||
<Icon.Trash className="w-4 h-auto mr-2" /> {t("common.delete")}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
></Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full p-3 flex flex-row justify-start items-start flex-wrap gap-3">
|
||||
{shortcuts.map((shortcut) => {
|
||||
return (
|
||||
<ShortcutView
|
||||
key={shortcut.id}
|
||||
className="!w-auto"
|
||||
shortcut={shortcut}
|
||||
alwaysShowLink={!sm}
|
||||
onClick={() => handleShortcutClick(shortcut)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showEditDialog && (
|
||||
<CreateCollectionDialog
|
||||
collectionId={collection.id}
|
||||
onClose={() => setShowEditDialog(false)}
|
||||
onConfirm={() => setShowEditDialog(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionView;
|
259
frontend/web/src/components/CreateCollectionDialog.tsx
Normal file
259
frontend/web/src/components/CreateCollectionDialog.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
import { Button, Input, Modal, ModalDialog, Radio, RadioGroup } from "@mui/joy";
|
||||
import { isUndefined } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "@/stores";
|
||||
import useCollectionStore from "@/stores/v1/collection";
|
||||
import { Collection } from "@/types/proto/api/v2/collection_service";
|
||||
import { Visibility } from "@/types/proto/api/v2/common";
|
||||
import { convertVisibilityFromPb } from "@/utils/visibility";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import Icon from "./Icon";
|
||||
import ShortcutView from "./ShortcutView";
|
||||
|
||||
interface Props {
|
||||
collectionId?: number;
|
||||
onClose: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
collectionCreate: Collection;
|
||||
}
|
||||
|
||||
const CreateCollectionDialog: React.FC<Props> = (props: Props) => {
|
||||
const { onClose, onConfirm, collectionId } = props;
|
||||
const { t } = useTranslation();
|
||||
const collectionStore = useCollectionStore();
|
||||
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
||||
const [state, setState] = useState<State>({
|
||||
collectionCreate: Collection.fromPartial({
|
||||
visibility: Visibility.PRIVATE,
|
||||
}),
|
||||
});
|
||||
const [selectedShortcuts, setSelectedShortcuts] = useState<Shortcut[]>([]);
|
||||
const requestState = useLoading(false);
|
||||
const isCreating = isUndefined(collectionId);
|
||||
const unselectedShortcuts = shortcutList
|
||||
.filter((shortcut) => (state.collectionCreate.visibility === Visibility.PUBLIC ? shortcut.visibility === "PUBLIC" : true))
|
||||
.filter((shortcut) => !selectedShortcuts.find((selectedShortcut) => selectedShortcut.id === shortcut.id));
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (collectionId) {
|
||||
const collection = await collectionStore.getOrFetchCollectionById(collectionId);
|
||||
if (collection) {
|
||||
setState({
|
||||
...state,
|
||||
collectionCreate: Object.assign(state.collectionCreate, {
|
||||
...collection,
|
||||
}),
|
||||
});
|
||||
setSelectedShortcuts(
|
||||
collection.shortcutIds
|
||||
.map((shortcutId) => shortcutList.find((shortcut) => shortcut.id === shortcutId))
|
||||
.filter(Boolean) as Shortcut[]
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [collectionId]);
|
||||
|
||||
const setPartialState = (partialState: Partial<State>) => {
|
||||
setState({
|
||||
...state,
|
||||
...partialState,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
collectionCreate: Object.assign(state.collectionCreate, {
|
||||
name: e.target.value.replace(/\s+/g, "-"),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
collectionCreate: Object.assign(state.collectionCreate, {
|
||||
title: e.target.value,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleVisibilityInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
collectionCreate: Object.assign(state.collectionCreate, {
|
||||
visibility: Number(e.target.value),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDescriptionInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPartialState({
|
||||
collectionCreate: Object.assign(state.collectionCreate, {
|
||||
description: e.target.value,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!state.collectionCreate.name || !state.collectionCreate.title) {
|
||||
toast.error("Please fill in required fields.");
|
||||
return;
|
||||
}
|
||||
if (selectedShortcuts.length === 0) {
|
||||
toast.error("Please select at least one shortcut.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isCreating) {
|
||||
await collectionStore.updateCollection(
|
||||
{
|
||||
id: collectionId,
|
||||
name: state.collectionCreate.name,
|
||||
title: state.collectionCreate.title,
|
||||
description: state.collectionCreate.description,
|
||||
visibility: state.collectionCreate.visibility,
|
||||
shortcutIds: selectedShortcuts.map((shortcut) => shortcut.id),
|
||||
},
|
||||
["name", "title", "description", "visibility", "shortcut_ids"]
|
||||
);
|
||||
} else {
|
||||
await collectionStore.createCollection({
|
||||
...state.collectionCreate,
|
||||
shortcutIds: selectedShortcuts.map((shortcut) => shortcut.id),
|
||||
});
|
||||
}
|
||||
|
||||
if (onConfirm) {
|
||||
onConfirm();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={true}>
|
||||
<ModalDialog>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<span className="text-lg font-medium">{isCreating ? "Create Collection" : "Edit Collection"}</span>
|
||||
<Button variant="plain" onClick={onClose}>
|
||||
<Icon.X className="w-5 h-auto text-gray-600" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-y-auto overflow-x-hidden w-80 sm:w-96 max-w-full">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Name <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="Should be an unique name and will be put in url"
|
||||
value={state.collectionCreate.name}
|
||||
onChange={handleNameInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">
|
||||
Title <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="A short title to describe your collection"
|
||||
value={state.collectionCreate.title}
|
||||
onChange={handleTitleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Description</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="A slightly longer description"
|
||||
value={state.collectionCreate.description}
|
||||
onChange={handleDescriptionInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Visibility</span>
|
||||
<div className="w-full flex flex-row justify-start items-center text-base">
|
||||
<RadioGroup orientation="horizontal" value={state.collectionCreate.visibility} onChange={handleVisibilityInputChange}>
|
||||
<Radio value={Visibility.PRIVATE} label={t(`shortcut.visibility.private.self`)} />
|
||||
<Radio value={Visibility.PUBLIC} label={t(`shortcut.visibility.public.self`)} />
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-gray-500 w-full bg-gray-100 border border-gray-200 dark:bg-zinc-800 dark:border-zinc-700 dark:text-gray-400 px-2 py-1 rounded-md">
|
||||
{t(`shortcut.visibility.${convertVisibilityFromPb(state.collectionCreate.visibility).toLowerCase()}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<p className="mb-2">
|
||||
<span>Shortcuts</span>
|
||||
<span className="opacity-60">({selectedShortcuts.length})</span>
|
||||
{selectedShortcuts.length === 0 && <span className="ml-2 italic opacity-80 text-sm">Select a shortcut first</span>}
|
||||
</p>
|
||||
<div className="w-full py-1 px-px flex flex-row justify-start items-start flex-wrap overflow-hidden gap-2">
|
||||
{selectedShortcuts.map((shortcut) => {
|
||||
return (
|
||||
<ShortcutView
|
||||
key={shortcut.id}
|
||||
className="!w-auto select-none max-w-[40%] cursor-pointer bg-gray-100 shadow dark:bg-zinc-800 dark:border-zinc-700 dark:text-gray-400"
|
||||
shortcut={shortcut}
|
||||
onClick={() => {
|
||||
setSelectedShortcuts([...selectedShortcuts.filter((selectedShortcut) => selectedShortcut.id !== shortcut.id)]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{unselectedShortcuts.map((shortcut) => {
|
||||
return (
|
||||
<ShortcutView
|
||||
key={shortcut.id}
|
||||
className="!w-auto select-none max-w-[40%] border-dashed cursor-pointer"
|
||||
shortcut={shortcut}
|
||||
onClick={() => {
|
||||
setSelectedShortcuts([...selectedShortcuts, shortcut]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{selectedShortcuts.length + unselectedShortcuts.length === 0 && (
|
||||
<div className="w-full flex flex-row justify-center items-center text-gray-400">
|
||||
<Icon.PackageOpen className="w-6 h-auto" />
|
||||
<p className="ml-2">No shortcuts found.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
|
||||
<Button color="neutral" variant="plain" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button color="primary" disabled={requestState.isLoading} loading={requestState.isLoading} onClick={handleSaveBtnClick}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateCollectionDialog;
|
@ -1,15 +1,17 @@
|
||||
import { Button, Divider, Input, Modal, ModalDialog, Radio, RadioGroup, Textarea } from "@mui/joy";
|
||||
import classnames from "classnames";
|
||||
import { isUndefined } from "lodash-es";
|
||||
import { isUndefined, uniq } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "@/stores";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { shortcutService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface Props {
|
||||
shortcutId?: ShortcutId;
|
||||
initialShortcut?: Partial<Shortcut>;
|
||||
onClose: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
@ -21,8 +23,9 @@ interface State {
|
||||
const visibilities: Visibility[] = ["PRIVATE", "WORKSPACE", "PUBLIC"];
|
||||
|
||||
const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
const { onClose, onConfirm, shortcutId } = props;
|
||||
const { onClose, onConfirm, shortcutId, initialShortcut } = props;
|
||||
const { t } = useTranslation();
|
||||
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
||||
const [state, setState] = useState<State>({
|
||||
shortcutCreate: {
|
||||
name: "",
|
||||
@ -36,11 +39,13 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
description: "",
|
||||
image: "",
|
||||
},
|
||||
...initialShortcut,
|
||||
},
|
||||
});
|
||||
const [showAdditionalFields, setShowAdditionalFields] = useState<boolean>(false);
|
||||
const [showOpenGraphMetadata, setShowOpenGraphMetadata] = useState<boolean>(false);
|
||||
const [tag, setTag] = useState<string>("");
|
||||
const tagSuggestions = uniq(shortcutList.map((shortcut) => shortcut.tags).flat());
|
||||
const requestState = useLoading(false);
|
||||
const isCreating = isUndefined(shortcutId);
|
||||
|
||||
@ -149,9 +154,17 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleTagSuggestionsClick = (suggestion: string) => {
|
||||
if (tag === "") {
|
||||
setTag(suggestion);
|
||||
} else {
|
||||
setTag(`${tag} ${suggestion}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!state.shortcutCreate.name) {
|
||||
toast.error("Name is required");
|
||||
if (!state.shortcutCreate.name || !state.shortcutCreate.link) {
|
||||
toast.error("Please fill in required fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -188,7 +201,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<Modal open={true}>
|
||||
<ModalDialog>
|
||||
<div className="flex flex-row justify-between items-center w-80 sm:w-96 mb-4">
|
||||
<div className="flex flex-row justify-between items-center w-80 sm:w-96">
|
||||
<span className="text-lg font-medium">{isCreating ? "Create Shortcut" : "Edit Shortcut"}</span>
|
||||
<Button variant="plain" onClick={onClose}>
|
||||
<Icon.X className="w-5 h-auto text-gray-600" />
|
||||
@ -196,19 +209,23 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
<div className="overflow-y-auto overflow-x-hidden">
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Name</span>
|
||||
<span className="mb-2">
|
||||
Name <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
placeholder="Unique shortcut name"
|
||||
placeholder="Should be an unique name and will be put in url"
|
||||
value={state.shortcutCreate.name}
|
||||
onChange={handleNameInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Destination URL</span>
|
||||
<span className="mb-2">
|
||||
Destination URL <span className="text-red-600">*</span>
|
||||
</span>
|
||||
<Input
|
||||
className="w-full"
|
||||
type="text"
|
||||
@ -220,6 +237,22 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Tags</span>
|
||||
<Input className="w-full" type="text" placeholder="github slash" value={tag} onChange={handleTagsInputChange} />
|
||||
{tagSuggestions.length > 0 && (
|
||||
<div className="w-full flex flex-row justify-start items-start mt-2">
|
||||
<Icon.Asterisk className="w-4 h-auto shrink-0 mx-1 text-gray-400 dark:text-gray-500" />
|
||||
<div className="w-auto flex flex-row justify-start items-start flex-wrap gap-x-2 gap-y-1">
|
||||
{tagSuggestions.map((tag) => (
|
||||
<span
|
||||
className="text-gray-600 dark:text-gray-500 cursor-pointer max-w-[6rem] truncate block text-sm flex-nowrap leading-4 hover:text-black dark:hover:text-gray-400"
|
||||
key={tag}
|
||||
onClick={() => handleTagSuggestionsClick(tag)}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Visibility</span>
|
||||
@ -234,7 +267,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
{t(`shortcut.visibility.${state.shortcutCreate.visibility.toLowerCase()}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
<Divider className="text-gray-500">Optional</Divider>
|
||||
<Divider className="text-gray-500">More</Divider>
|
||||
<div className="w-full flex flex-col justify-start items-start border rounded-md overflow-hidden my-3 dark:border-zinc-800">
|
||||
<div
|
||||
className={classnames(
|
||||
@ -285,7 +318,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
>
|
||||
<span className="text-sm flex flex-row justify-start items-center">
|
||||
Social media metadata
|
||||
<Icon.Sparkles className="ml-1 w-4 h-auto text-blue-600" />
|
||||
<Icon.Sparkles className="w-4 h-auto shrink-0 ml-1 text-blue-600 dark:text-blue-500" />
|
||||
</span>
|
||||
<button className="w-7 h-7 p-1 rounded-md">
|
||||
<Icon.ChevronDown className={classnames("w-4 h-auto text-gray-500", showOpenGraphMetadata ? "transform rotate-180" : "")} />
|
||||
|
@ -9,7 +9,7 @@ const DemoBanner: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="z-10 relative 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-6xl px-4 md:px-12 flex flex-row justify-between items-center gap-x-3">
|
||||
<div className="w-full max-w-8xl px-4 md:px-12 flex flex-row justify-between items-center gap-x-3">
|
||||
<span>✨🔗 Slash - An open source, self-hosted bookmarks and link sharing platform</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"
|
||||
|
@ -66,11 +66,11 @@ const EditUserinfoDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
<div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Email</span>
|
||||
<span className="mb-2">{t("common.email")}</span>
|
||||
<Input className="w-full" type="text" value={email} onChange={handleEmailChanged} />
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start mb-3">
|
||||
<span className="mb-2">Nickname</span>
|
||||
<span className="mb-2">{t("user.nickname")}</span>
|
||||
<Input className="w-full" type="text" value={nickname} onChange={handleNicknameChanged} />
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-end items-center space-x-2">
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Avatar } from "@mui/joy";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import useWorkspaceStore from "@/stores/v1/workspace";
|
||||
import { PlanType } from "@/types/proto/api/v2/subscription_service";
|
||||
import * as api from "../helpers/api";
|
||||
@ -10,11 +11,14 @@ import Icon from "./Icon";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const currentUser = useUserStore().getCurrentUser();
|
||||
const [showAboutDialog, setShowAboutDialog] = useState<boolean>(false);
|
||||
const profile = workspaceStore.profile;
|
||||
const isAdmin = currentUser.role === "ADMIN";
|
||||
const shouldShowRouterSwitch = location.pathname === "/" || location.pathname === "/collections";
|
||||
|
||||
const handleSignOutButtonClick = async () => {
|
||||
await api.signout();
|
||||
@ -23,11 +27,11 @@ const Header: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full bg-gray-50 dark:bg-zinc-900 border-b border-b-gray-200 dark:border-b-zinc-800">
|
||||
<div className="w-full max-w-6xl mx-auto px-3 md:px-12 py-5 flex flex-row justify-between items-center">
|
||||
<div className="w-full bg-gray-50 dark:bg-zinc-800 border-b border-b-gray-200 dark:border-b-zinc-800">
|
||||
<div className="w-full max-w-8xl mx-auto px-3 md:px-12 py-3 flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row justify-start items-center shrink mr-2">
|
||||
<Link to="/" className="text-lg cursor-pointer flex flex-row justify-start items-center dark:text-gray-400">
|
||||
<img id="logo-img" src="/logo.png" className="w-8 h-auto mr-2 -mt-0.5 dark:opacity-80" alt="" />
|
||||
<Link to="/" className="cursor-pointer flex flex-row justify-start items-center dark:text-gray-400">
|
||||
<img id="logo-img" src="/logo.png" className="w-7 h-auto mr-2 -mt-0.5 dark:opacity-80 rounded-full shadow" alt="" />
|
||||
Slash
|
||||
</Link>
|
||||
{profile.plan === PlanType.PRO && (
|
||||
@ -35,6 +39,36 @@ const Header: React.FC = () => {
|
||||
PRO
|
||||
</span>
|
||||
)}
|
||||
{shouldShowRouterSwitch && (
|
||||
<>
|
||||
<span className="font-mono opacity-60 mx-1">/</span>
|
||||
<Dropdown
|
||||
trigger={
|
||||
<button className="flex flex-row justify-end items-center cursor-pointer">
|
||||
<span className="dark:text-gray-400">{location.pathname === "/" ? "Shortcuts" : "Collections"}</span>
|
||||
<Icon.ChevronsUpDown className="ml-1 w-4 h-auto text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
}
|
||||
actionsClassName="!w-36 -left-4"
|
||||
actions={
|
||||
<>
|
||||
<Link
|
||||
to="/"
|
||||
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||
>
|
||||
<Icon.SquareSlash className="w-5 h-auto mr-2 opacity-70" /> Shortcuts
|
||||
</Link>
|
||||
<Link
|
||||
to="/collections"
|
||||
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||
>
|
||||
<Icon.LibrarySquare className="w-5 h-auto mr-2 opacity-70" /> Collections
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
></Dropdown>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative flex-shrink-0">
|
||||
<Dropdown
|
||||
@ -52,27 +86,27 @@ const Header: React.FC = () => {
|
||||
to="/setting/general"
|
||||
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||
>
|
||||
<Icon.User className="w-4 h-auto mr-2" /> Profile
|
||||
<Icon.User className="w-4 h-auto mr-2" /> {t("user.profile")}
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link
|
||||
to="/setting/workspace"
|
||||
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||
>
|
||||
<Icon.Settings className="w-4 h-auto mr-2" /> Setting
|
||||
<Icon.Settings className="w-4 h-auto mr-2" /> {t("settings.self")}
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||
onClick={() => setShowAboutDialog(true)}
|
||||
>
|
||||
<Icon.Info className="w-4 h-auto mr-2" /> About
|
||||
<Icon.Info className="w-4 h-auto mr-2" /> {t("common.about")}
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-2 flex flex-row justify-start items-center text-left dark:text-gray-400 leading-8 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-800 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||
onClick={() => handleSignOutButtonClick()}
|
||||
>
|
||||
<Icon.LogOut className="w-4 h-auto mr-2" /> Sign out
|
||||
<Icon.LogOut className="w-4 h-auto mr-2" /> {t("auth.sign-out")}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
@ -1,68 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import { useAppSelector } from "../stores";
|
||||
import useViewStore from "../stores/v1/view";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const Navigator = () => {
|
||||
const viewStore = useViewStore();
|
||||
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
||||
const tags = shortcutList.map((shortcut) => shortcut.tags).flat();
|
||||
const currentTab = viewStore.filter.tab || `tab:all`;
|
||||
const sortedTagMap = sortTags(tags);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-start items-center mb-4 gap-1 sm:flex-wrap overflow-x-auto no-scrollbar">
|
||||
<button
|
||||
className={classNames(
|
||||
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
|
||||
currentTab === "tab:all"
|
||||
? "bg-gray-600 dark:bg-zinc-700 text-white dark:text-gray-400 shadow"
|
||||
: "hover:bg-gray-200 dark:hover:bg-zinc-700"
|
||||
)}
|
||||
onClick={() => viewStore.setFilter({ tab: "tab:all" })}
|
||||
>
|
||||
<Icon.CircleSlash className="w-4 h-auto mr-1" />
|
||||
<span className="font-normal">All</span>
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
|
||||
currentTab === "tab:mine"
|
||||
? "bg-gray-600 dark:bg-zinc-700 text-white dark:text-gray-400 shadow"
|
||||
: "hover:bg-gray-200 dark:hover:bg-zinc-700"
|
||||
)}
|
||||
onClick={() => viewStore.setFilter({ tab: "tab:mine" })}
|
||||
>
|
||||
<Icon.User className="w-4 h-auto mr-1" />
|
||||
<span className="font-normal">Mine</span>
|
||||
</button>
|
||||
{Array.from(sortedTagMap.keys()).map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
className={classNames(
|
||||
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
|
||||
currentTab === `tag:${tag}`
|
||||
? "bg-gray-600 dark:bg-zinc-700 text-white dark:text-gray-400 shadow"
|
||||
: "hover:bg-gray-200 dark:hover:bg-zinc-700"
|
||||
)}
|
||||
onClick={() => viewStore.setFilter({ tab: `tag:${tag}`, tag: undefined })}
|
||||
>
|
||||
<Icon.Hash className="w-4 h-auto mr-0.5" />
|
||||
<span className="max-w-[8rem] truncate font-normal">{tag}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const sortTags = (tags: string[]): Map<string, number> => {
|
||||
const map = new Map<string, number>();
|
||||
for (const tag of tags) {
|
||||
const count = map.get(tag) || 0;
|
||||
map.set(tag, count + 1);
|
||||
}
|
||||
const sortedMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
|
||||
return sortedMap;
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default Navigator;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { shortcutService } from "../services";
|
||||
import useUserStore from "../stores/v1/user";
|
||||
import { showCommonDialog } from "./Alert";
|
||||
@ -16,7 +16,7 @@ interface Props {
|
||||
const ShortcutActionsDropdown = (props: Props) => {
|
||||
const { shortcut } = props;
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigateTo = useNavigateTo();
|
||||
const currentUser = useUserStore().getCurrentUser();
|
||||
const [showEditDialog, setShowEditDialog] = useState<boolean>(false);
|
||||
const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false);
|
||||
@ -34,7 +34,7 @@ const ShortcutActionsDropdown = (props: Props) => {
|
||||
};
|
||||
|
||||
const gotoAnalytics = () => {
|
||||
navigate(`/shortcut/${shortcut.id}#analytics`);
|
||||
navigateTo(`/shortcut/${shortcut.id}#analytics`);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { Tooltip } from "@mui/joy";
|
||||
import classNames from "classnames";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { absolutifyLink } from "../helpers/utils";
|
||||
import useFaviconStore from "../stores/v1/favicon";
|
||||
import { absolutifyLink, getFaviconWithGoogleS2 } from "../helpers/utils";
|
||||
import useViewStore from "../stores/v1/view";
|
||||
import Icon from "./Icon";
|
||||
import ShortcutActionsDropdown from "./ShortcutActionsDropdown";
|
||||
@ -16,21 +14,12 @@ interface Props {
|
||||
shortcut: Shortcut;
|
||||
}
|
||||
|
||||
const ShortcutView = (props: Props) => {
|
||||
const ShortcutCard = (props: Props) => {
|
||||
const { shortcut } = props;
|
||||
const { t } = useTranslation();
|
||||
const viewStore = useViewStore();
|
||||
const faviconStore = useFaviconStore();
|
||||
const [favicon, setFavicon] = useState<string | undefined>(undefined);
|
||||
const shortcutLink = absolutifyLink(`/s/${shortcut.name}`);
|
||||
|
||||
useEffect(() => {
|
||||
faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => {
|
||||
if (url) {
|
||||
setFavicon(url);
|
||||
}
|
||||
});
|
||||
}, [shortcut.link]);
|
||||
const favicon = getFaviconWithGoogleS2(shortcut.link);
|
||||
|
||||
const handleCopyButtonClick = () => {
|
||||
copy(shortcutLink);
|
||||
@ -48,7 +37,7 @@ const ShortcutView = (props: Props) => {
|
||||
<div className="w-[calc(100%-16px)] flex flex-row justify-start items-center mr-1 shrink-0">
|
||||
<Link to={`/shortcut/${shortcut.id}`} className={classNames("w-8 h-8 flex justify-center items-center overflow-clip shrink-0")}>
|
||||
{favicon ? (
|
||||
<img className="w-full h-auto rounded-full" src={favicon} decoding="async" loading="lazy" />
|
||||
<img className="w-full h-auto rounded-lg" src={favicon} decoding="async" loading="lazy" />
|
||||
) : (
|
||||
<Icon.CircleSlash className="w-full h-auto text-gray-400" />
|
||||
)}
|
||||
@ -63,9 +52,9 @@ const ShortcutView = (props: Props) => {
|
||||
href={shortcutLink}
|
||||
>
|
||||
<div className="truncate">
|
||||
<span>{shortcut.title}</span>
|
||||
<span className="dark:text-gray-400">{shortcut.title}</span>
|
||||
{shortcut.title ? (
|
||||
<span className="text-gray-400">(s/{shortcut.name})</span>
|
||||
<span className="text-gray-500">(s/{shortcut.name})</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gray-400 dark:text-gray-500">s/</span>
|
||||
@ -104,38 +93,32 @@ const ShortcutView = (props: Props) => {
|
||||
return (
|
||||
<span
|
||||
key={tag}
|
||||
className="max-w-[8rem] truncate text-gray-400 dark:text-gray-500 text-sm font-mono leading-4 cursor-pointer hover:opacity-80"
|
||||
className="max-w-[8rem] truncate text-gray-400 dark:text-gray-500 text-sm leading-4 cursor-pointer hover:opacity-80"
|
||||
onClick={() => viewStore.setFilter({ tag: tag })}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm font-mono leading-4 italic">No tags</span>}
|
||||
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm leading-4 italic">No tags</span>}
|
||||
</div>
|
||||
<div className="w-full flex mt-2 gap-2">
|
||||
<Tooltip title="Creator" variant="solid" placement="top" arrow>
|
||||
<div className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full text-gray-500 text-sm dark:border-zinc-800">
|
||||
<Icon.User className="w-4 h-auto mr-1" />
|
||||
<span className="max-w-[4rem] sm:max-w-[6rem] truncate">{shortcut.creator.nickname}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="w-full flex mt-2 gap-2 overflow-x-auto">
|
||||
<Tooltip title={t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.description`)} variant="solid" placement="top" arrow>
|
||||
<div
|
||||
className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full cursor-pointer text-gray-500 text-sm dark:border-zinc-800"
|
||||
className="w-auto px-2 leading-6 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap border rounded-full cursor-pointer text-gray-500 dark:text-gray-400 text-sm dark:border-zinc-700"
|
||||
onClick={() => viewStore.setFilter({ visibility: shortcut.visibility })}
|
||||
>
|
||||
<VisibilityIcon className="w-4 h-auto mr-1" visibility={shortcut.visibility} />
|
||||
<VisibilityIcon className="w-4 h-auto mr-1 opacity-60" visibility={shortcut.visibility} />
|
||||
{t(`shortcut.visibility.${shortcut.visibility.toLowerCase()}.self`)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title="View count" variant="solid" placement="top" arrow>
|
||||
<Link
|
||||
to={`/shortcut/${shortcut.id}#analytics`}
|
||||
className="w-auto px-2 leading-6 flex flex-row justify-start items-center border rounded-full cursor-pointer text-gray-500 text-sm dark:border-zinc-800"
|
||||
className="w-auto px-2 leading-6 flex flex-row justify-start items-center flex-nowrap whitespace-nowrap border rounded-full cursor-pointer text-gray-500 dark:text-gray-400 text-sm dark:border-zinc-700"
|
||||
>
|
||||
<Icon.BarChart2 className="w-4 h-auto mr-1" />
|
||||
{shortcut.view} visits
|
||||
<Icon.BarChart2 className="w-4 h-auto mr-1 opacity-80" />
|
||||
{t("shortcut.visits", { count: shortcut.view })}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -144,4 +127,4 @@ const ShortcutView = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ShortcutView;
|
||||
export default ShortcutCard;
|
||||
|
@ -1,78 +1,66 @@
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { absolutifyLink } from "../helpers/utils";
|
||||
import useFaviconStore from "../stores/v1/favicon";
|
||||
import { getFaviconWithGoogleS2 } from "../helpers/utils";
|
||||
import Icon from "./Icon";
|
||||
import ShortcutActionsDropdown from "./ShortcutActionsDropdown";
|
||||
|
||||
interface Props {
|
||||
shortcut: Shortcut;
|
||||
className?: string;
|
||||
showActions?: boolean;
|
||||
alwaysShowLink?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const ShortcutView = (props: Props) => {
|
||||
const { shortcut } = props;
|
||||
const faviconStore = useFaviconStore();
|
||||
const [favicon, setFavicon] = useState<string | undefined>(undefined);
|
||||
const shortcutLink = absolutifyLink(`/s/${shortcut.name}`);
|
||||
|
||||
useEffect(() => {
|
||||
faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => {
|
||||
if (url) {
|
||||
setFavicon(url);
|
||||
}
|
||||
});
|
||||
}, [shortcut.link]);
|
||||
const { shortcut, className, showActions, alwaysShowLink, onClick } = props;
|
||||
const favicon = getFaviconWithGoogleS2(shortcut.link);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
"group w-full px-3 py-2 flex flex-col justify-start items-start border rounded-lg hover:bg-gray-100 hover:shadow dark:border-zinc-800 dark:hover:bg-zinc-800"
|
||||
"group w-full px-3 py-2 flex flex-row justify-start items-center border rounded-lg hover:bg-gray-100 dark:border-zinc-800 dark:hover:bg-zinc-800",
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="w-[calc(100%-16px)] flex flex-row justify-start items-center mr-1 shrink-0">
|
||||
<Link to={`/shortcut/${shortcut.id}`} className={classNames("w-5 h-5 flex justify-center items-center overflow-clip shrink-0")}>
|
||||
<div className={classNames("w-5 h-5 flex justify-center items-center overflow-clip shrink-0")}>
|
||||
{favicon ? (
|
||||
<img className="w-full h-auto rounded-full" src={favicon} decoding="async" loading="lazy" />
|
||||
<img className="w-full h-auto rounded-lg" src={favicon} decoding="async" loading="lazy" />
|
||||
) : (
|
||||
<Icon.CircleSlash className="w-full h-auto text-gray-400" />
|
||||
)}
|
||||
</Link>
|
||||
<div className="ml-1 w-[calc(100%-20px)] flex flex-col justify-start items-start">
|
||||
<div className="w-full flex flex-row justify-start items-center">
|
||||
<a
|
||||
className={classNames(
|
||||
"max-w-full flex flex-row px-1 mr-1 justify-start items-center cursor-pointer rounded-md hover:underline"
|
||||
)}
|
||||
href={shortcutLink}
|
||||
target="_blank"
|
||||
>
|
||||
<div className="truncate">
|
||||
<span>{shortcut.title}</span>
|
||||
</div>
|
||||
<div className="ml-2 w-full truncate">
|
||||
{shortcut.title ? (
|
||||
<span className="text-gray-400">(s/{shortcut.name})</span>
|
||||
<>
|
||||
<span className="dark:text-gray-400">{shortcut.title}</span>
|
||||
<span className="text-gray-500">(s/{shortcut.name})</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gray-400 dark:text-gray-500">s/</span>
|
||||
<span className="truncate dark:text-gray-400">{shortcut.name}</span>
|
||||
<span className="dark:text-gray-400">{shortcut.name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span className="hidden group-hover:block ml-1 cursor-pointer shrink-0">
|
||||
<Icon.ExternalLink className="w-4 h-auto text-gray-600" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center">
|
||||
<Link
|
||||
className={classNames(
|
||||
"hidden group-hover:block ml-1 w-6 h-6 p-1 shrink-0 rounded-lg bg-gray-200 dark:bg-zinc-900 hover:opacity-80",
|
||||
alwaysShowLink && "!block"
|
||||
)}
|
||||
to={`/s/${shortcut.name}`}
|
||||
target="_blank"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Icon.ArrowUpRight className="w-4 h-auto text-gray-400 shrink-0" />
|
||||
</Link>
|
||||
{showActions && (
|
||||
<div className="ml-1 flex flex-row justify-end items-center shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
<ShortcutActionsDropdown shortcut={shortcut} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import { absolutifyLink } from "@/helpers/utils";
|
||||
import useViewStore from "../stores/v1/view";
|
||||
import ShortcutCard from "./ShortcutCard";
|
||||
import ShortcutView from "./ShortcutView";
|
||||
@ -13,15 +14,19 @@ const ShortcutsContainer: React.FC<Props> = (props: Props) => {
|
||||
const displayStyle = viewStore.displayStyle || "full";
|
||||
const ShortcutItemView = viewStore.displayStyle === "compact" ? ShortcutView : ShortcutCard;
|
||||
|
||||
const handleShortcutClick = (shortcut: Shortcut) => {
|
||||
window.open(absolutifyLink(`/s/${shortcut.name}`));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"w-full grid grid-cols-1 gap-y-2 sm:gap-2",
|
||||
displayStyle === "full" ? "sm:grid-cols-2" : "grid-cols-2 sm:grid-cols-4 gap-2"
|
||||
"w-full grid grid-cols-1 gap-3 sm:gap-4",
|
||||
displayStyle === "full" ? "sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" : "grid-cols-2 sm:grid-cols-4"
|
||||
)}
|
||||
>
|
||||
{shortcutList.map((shortcut) => {
|
||||
return <ShortcutItemView key={shortcut.id} shortcut={shortcut} />;
|
||||
return <ShortcutItemView key={shortcut.id} shortcut={shortcut} showActions={true} onClick={() => handleShortcutClick(shortcut)} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
70
frontend/web/src/components/ShortcutsNavigator.tsx
Normal file
70
frontend/web/src/components/ShortcutsNavigator.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppSelector } from "../stores";
|
||||
import useViewStore from "../stores/v1/view";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const ShortcutsNavigator = () => {
|
||||
const { t } = useTranslation();
|
||||
const viewStore = useViewStore();
|
||||
const { shortcutList } = useAppSelector((state) => state.shortcut);
|
||||
const tags = shortcutList.map((shortcut) => shortcut.tags).flat();
|
||||
const currentTab = viewStore.filter.tab || `tab:all`;
|
||||
const sortedTagMap = sortTags(tags);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-start items-center mb-4 gap-1 sm:flex-wrap overflow-x-auto no-scrollbar">
|
||||
<button
|
||||
className={classNames(
|
||||
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
|
||||
currentTab === "tab:all"
|
||||
? "bg-gray-600 dark:bg-zinc-700 text-white dark:text-gray-400 shadow"
|
||||
: "hover:bg-gray-200 dark:hover:bg-zinc-700"
|
||||
)}
|
||||
onClick={() => viewStore.setFilter({ tab: "tab:all" })}
|
||||
>
|
||||
<Icon.CircleSlash className="w-4 h-auto mr-1" />
|
||||
<span className="font-normal">{t("filter.all")}</span>
|
||||
</button>
|
||||
<button
|
||||
className={classNames(
|
||||
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
|
||||
currentTab === "tab:mine"
|
||||
? "bg-gray-600 dark:bg-zinc-700 text-white dark:text-gray-400 shadow"
|
||||
: "hover:bg-gray-200 dark:hover:bg-zinc-700"
|
||||
)}
|
||||
onClick={() => viewStore.setFilter({ tab: "tab:mine" })}
|
||||
>
|
||||
<Icon.User className="w-4 h-auto mr-1" />
|
||||
<span className="font-normal">{t("filter.mine")}</span>
|
||||
</button>
|
||||
{Array.from(sortedTagMap.keys()).map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
className={classNames(
|
||||
"flex flex-row justify-center items-center px-2 leading-7 text-sm dark:text-gray-400 rounded-md",
|
||||
currentTab === `tag:${tag}`
|
||||
? "bg-gray-600 dark:bg-zinc-700 text-white dark:text-gray-400 shadow"
|
||||
: "hover:bg-gray-200 dark:hover:bg-zinc-700"
|
||||
)}
|
||||
onClick={() => viewStore.setFilter({ tab: `tag:${tag}`, tag: undefined })}
|
||||
>
|
||||
<Icon.Hash className="w-4 h-auto mr-0.5" />
|
||||
<span className="max-w-[8rem] truncate font-normal">{tag}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const sortTags = (tags: string[]): Map<string, number> => {
|
||||
const map = new Map<string, number>();
|
||||
for (const tag of tags) {
|
||||
const count = map.get(tag) || 0;
|
||||
map.set(tag, count + 1);
|
||||
}
|
||||
const sortedMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
|
||||
return sortedMap;
|
||||
};
|
||||
|
||||
export default ShortcutsNavigator;
|
@ -22,7 +22,7 @@ const SubscriptionFAQ = () => {
|
||||
<Accordion>
|
||||
<AccordionSummary>{`Can I get a refund if Slash doesn't meet my needs?`}</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
Yes, absolutely! You can send a email to me at `stevenlgtm@gmail.com`. I will refund you as soon as possible.
|
||||
Yes, absolutely! You can send a email to me at `yourselfhosted@gmail.com`. I will refund you as soon as possible.
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Divider, Option, Select, Switch } from "@mui/joy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useViewStore from "../stores/v1/view";
|
||||
import Icon from "./Icon";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
|
||||
const ViewSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
const viewStore = useViewStore();
|
||||
const order = viewStore.getOrder();
|
||||
const { field, direction } = order;
|
||||
@ -20,7 +22,7 @@ const ViewSetting = () => {
|
||||
actions={
|
||||
<div className="w-52 p-2 gap-2 flex flex-col justify-start items-start" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<span className="text-sm shrink-0 mr-2">Compact mode</span>
|
||||
<span className="text-sm shrink-0 mr-2">{t("filter.compact-mode")}</span>
|
||||
<Switch
|
||||
size="sm"
|
||||
checked={displayStyle === "compact"}
|
||||
@ -29,7 +31,7 @@ const ViewSetting = () => {
|
||||
</div>
|
||||
<Divider className="!my-1" />
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<span className="text-sm shrink-0 mr-2">Order by</span>
|
||||
<span className="text-sm shrink-0 mr-2">{t("filter.order-by")}</span>
|
||||
<Select size="sm" value={field} onChange={(_, value) => viewStore.setOrder({ field: value as any })}>
|
||||
<Option value={"name"}>Name</Option>
|
||||
<Option value={"updatedTs"}>CreatedAt</Option>
|
||||
@ -38,7 +40,7 @@ const ViewSetting = () => {
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<span className="text-sm shrink-0 mr-2">Direction</span>
|
||||
<span className="text-sm shrink-0 mr-2">{t("filter.direction")}</span>
|
||||
<Select size="sm" value={direction} onChange={(_, value) => viewStore.setOrder({ direction: value as any })}>
|
||||
<Option value={"asc"}>ASC</Option>
|
||||
<Option value={"desc"}>DESC</Option>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import useToggle from "../../hooks/useToggle";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import Icon from "../Icon";
|
||||
|
||||
interface Props {
|
||||
@ -11,14 +10,14 @@ interface Props {
|
||||
|
||||
const Dropdown: React.FC<Props> = (props: Props) => {
|
||||
const { trigger, actions, className, actionsClassName } = props;
|
||||
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
|
||||
const [dropdownStatus, setDropdownStatus] = useState(false);
|
||||
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (dropdownStatus) {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!dropdownWrapperRef.current?.contains(event.target as Node)) {
|
||||
toggleDropdownStatus(false);
|
||||
setDropdownStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -35,7 +34,7 @@ const Dropdown: React.FC<Props> = (props: Props) => {
|
||||
|
||||
const handleToggleDropdownStatus = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
toggleDropdownStatus();
|
||||
setDropdownStatus(!dropdownStatus);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -15,7 +15,7 @@ const AccountSection: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col justify-start items-start gap-y-2">
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">Account</p>
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">{t("common.account")}</p>
|
||||
<p className="flex flex-row justify-start items-center mt-2 dark:text-gray-400">
|
||||
<span className="text-xl">{currentUser.nickname}</span>
|
||||
{isAdmin && <span className="ml-2 bg-blue-600 text-white px-2 leading-6 text-sm rounded-full">Admin</span>}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { Button, IconButton } from "@mui/joy";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useUserStore from "../../stores/v1/user";
|
||||
import { showCommonDialog } from "../Alert";
|
||||
import CreateUserDialog from "../CreateUserDialog";
|
||||
import Icon from "../Icon";
|
||||
|
||||
const MemberSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const userStore = useUserStore();
|
||||
const [showCreateUserDialog, setShowCreateUserDialog] = useState<boolean>(false);
|
||||
const [currentEditingUser, setCurrentEditingUser] = useState<User | undefined>(undefined);
|
||||
@ -43,7 +45,7 @@ const MemberSection = () => {
|
||||
<div className="w-full">
|
||||
<div className="sm:flex sm:items-center">
|
||||
<div className="sm:flex-auto">
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">Users</p>
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">{t("user.self")}</p>
|
||||
<p className="mt-2 text-sm text-gray-700 dark:text-gray-600">
|
||||
A list of all the users in your workspace including their nickname, email and role.
|
||||
</p>
|
||||
@ -57,7 +59,7 @@ const MemberSection = () => {
|
||||
setCurrentEditingUser(undefined);
|
||||
}}
|
||||
>
|
||||
Add user
|
||||
{t("user.action.add-user")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,20 +70,20 @@ const MemberSection = () => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
|
||||
Nickname
|
||||
{t("user.nickname")}
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
|
||||
Email
|
||||
{t("user.email")}
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-500">
|
||||
Role
|
||||
{t("user.role")}
|
||||
</th>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4">
|
||||
<span className="sr-only">Edit</span>
|
||||
<span className="sr-only">{t("common.edit")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-zinc-800">
|
||||
{userList.map((user) => (
|
||||
<tr key={user.email}>
|
||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-gray-900 dark:text-gray-500">{user.nickname}</td>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Option, Select } from "@mui/joy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { releaseGuard } from "@/helpers/utils";
|
||||
import { UserSetting, UserSetting_ColorTheme, UserSetting_Locale } from "@/types/proto/api/v2/user_setting_service";
|
||||
import useUserStore from "../../stores/v1/user";
|
||||
import BetaBadge from "../BetaBadge";
|
||||
@ -61,10 +60,10 @@ const PreferenceSection: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col justify-start items-start gap-y-2">
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">Preference</p>
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">{t("settings.preference.self")}</p>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row justify-start items-center gap-x-1">
|
||||
<span className="dark:text-gray-400">Color Theme</span>
|
||||
<span className="dark:text-gray-400">{t("settings.preference.color-theme")}</span>
|
||||
</div>
|
||||
<Select defaultValue={colorTheme} onChange={(_, value) => handleSelectColorTheme(value as UserSetting_ColorTheme)}>
|
||||
{colorThemeOptions.map((option) => {
|
||||
@ -76,7 +75,6 @@ const PreferenceSection: React.FC = () => {
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
{releaseGuard() && (
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row justify-start items-center gap-x-1">
|
||||
<span className="dark:text-gray-400">{t("common.language")}</span>
|
||||
@ -92,7 +90,6 @@ const PreferenceSection: React.FC = () => {
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -2,11 +2,13 @@ import { Button, Checkbox, Textarea } from "@mui/joy";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { workspaceServiceClient } from "@/grpcweb";
|
||||
import useWorkspaceStore from "@/stores/v1/workspace";
|
||||
import { WorkspaceSetting } from "@/types/proto/api/v2/workspace_service";
|
||||
|
||||
const WorkspaceSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const [workspaceSetting, setWorkspaceSetting] = useState<WorkspaceSetting>(workspaceStore.setting);
|
||||
const originalWorkspaceSetting = useRef<WorkspaceSetting>(workspaceStore.setting);
|
||||
@ -56,11 +58,12 @@ const WorkspaceSection: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-4">
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">Workspace settings</p>
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">{t("settings.workspace.self")}</p>
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<p className="mt-2 dark:text-gray-400">Custom style</p>
|
||||
<p className="mt-2 dark:text-gray-400">{t("settings.workspace.custom-style")}</p>
|
||||
<Textarea
|
||||
className="w-full mt-2"
|
||||
placeholder="* {font-family: ui-monospace Monaco Consolas;}"
|
||||
minRows={2}
|
||||
maxRows={5}
|
||||
value={workspaceSetting.customStyle}
|
||||
@ -69,15 +72,15 @@ const WorkspaceSection: React.FC = () => {
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<Checkbox
|
||||
label="Enable user signup"
|
||||
label={t("settings.workspace.enable-user-signup.self")}
|
||||
checked={workspaceSetting.enableSignup}
|
||||
onChange={(event) => handleEnableSignUpChange(event.target.checked)}
|
||||
/>
|
||||
<p className="mt-2 text-gray-500">Once enabled, other users can signup.</p>
|
||||
<p className="mt-2 text-gray-500">{t("settings.workspace.enable-user-signup.description")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="outlined" color="neutral" disabled={!allowSave} onClick={handleSaveWorkspaceSetting}>
|
||||
Save
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { createChannel, createClientFactory, FetchTransport } from "nice-grpc-web";
|
||||
import { CollectionServiceDefinition } from "./types/proto/api/v2/collection_service";
|
||||
import { ShortcutServiceDefinition } from "./types/proto/api/v2/shortcut_service";
|
||||
import { SubscriptionServiceDefinition } from "./types/proto/api/v2/subscription_service";
|
||||
import { UserServiceDefinition } from "./types/proto/api/v2/user_service";
|
||||
import { UserSettingServiceDefinition } from "./types/proto/api/v2/user_setting_service";
|
||||
@ -22,3 +24,7 @@ export const workspaceServiceClient = clientFactory.create(WorkspaceServiceDefin
|
||||
export const userServiceClient = clientFactory.create(UserServiceDefinition, channel);
|
||||
|
||||
export const userSettingServiceClient = clientFactory.create(UserSettingServiceDefinition, channel);
|
||||
|
||||
export const shortcutServiceClient = clientFactory.create(ShortcutServiceDefinition, channel);
|
||||
|
||||
export const collectionServiceClient = clientFactory.create(CollectionServiceDefinition, channel);
|
||||
|
@ -71,7 +71,3 @@ export function patchShortcut(shortcutPatch: ShortcutPatch) {
|
||||
export function deleteShortcutById(shortcutId: ShortcutId) {
|
||||
return axios.delete(`/api/v1/shortcut/${shortcutId}`);
|
||||
}
|
||||
|
||||
export function getUrlFavicon(url: string) {
|
||||
return axios.get<string>(`/api/v1/url/favicon?url=${url}`);
|
||||
}
|
||||
|
@ -13,3 +13,12 @@ export const absolutifyLink = (rel: string): string => {
|
||||
export const releaseGuard = () => {
|
||||
return import.meta.env.MODE === "development";
|
||||
};
|
||||
|
||||
export const getFaviconWithGoogleS2 = (url: string) => {
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
return `https://www.google.com/s2/favicons?sz=128&domain=${urlObject.hostname}`;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
20
frontend/web/src/hooks/useNavigateTo.ts
Normal file
20
frontend/web/src/hooks/useNavigateTo.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NavigateOptions, useNavigate } from "react-router-dom";
|
||||
|
||||
const useNavigateTo = () => {
|
||||
const navigateTo = useNavigate();
|
||||
|
||||
const navigateToWithViewTransition = (to: string, options?: NavigateOptions) => {
|
||||
const document = window.document as any;
|
||||
if (!document.startViewTransition) {
|
||||
navigateTo(to, options);
|
||||
} else {
|
||||
document.startViewTransition(() => {
|
||||
navigateTo(to, options);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return navigateToWithViewTransition;
|
||||
};
|
||||
|
||||
export default useNavigateTo;
|
20
frontend/web/src/hooks/useResponsiveWidth.ts
Normal file
20
frontend/web/src/hooks/useResponsiveWidth.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import useWindowSize from "react-use/lib/useWindowSize";
|
||||
|
||||
enum TailwindResponsiveWidth {
|
||||
sm = 640,
|
||||
md = 768,
|
||||
lg = 1024,
|
||||
}
|
||||
|
||||
const useResponsiveWidth = () => {
|
||||
const { width } = useWindowSize();
|
||||
|
||||
return {
|
||||
sm: width >= TailwindResponsiveWidth.sm,
|
||||
md: width >= TailwindResponsiveWidth.md,
|
||||
lg: width >= TailwindResponsiveWidth.lg,
|
||||
};
|
||||
};
|
||||
|
||||
export default useResponsiveWidth;
|
@ -1,21 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
// Parameter is the boolean, with default "false" value
|
||||
const useToggle = (initialState = false): [boolean, (nextState?: boolean) => void] => {
|
||||
// Initialize the state
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
// Define and memorize toggler function in case we pass down the comopnent,
|
||||
// This function change the boolean value to it's opposite value
|
||||
const toggle = useCallback((nextState?: boolean) => {
|
||||
if (nextState !== undefined) {
|
||||
setState(nextState);
|
||||
} else {
|
||||
setState((state) => !state);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [state, toggle];
|
||||
};
|
||||
|
||||
export default useToggle;
|
@ -2,13 +2,15 @@ import { useColorScheme } from "@mui/joy";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { UserSetting_ColorTheme } from "@/types/proto/api/v2/user_setting_service";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Navigator from "@/components/Navigator";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { UserSetting_ColorTheme, UserSetting_Locale } from "@/types/proto/api/v2/user_setting_service";
|
||||
import Header from "../components/Header";
|
||||
import useUserStore from "../stores/v1/user";
|
||||
|
||||
const Root: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const navigateTo = useNavigateTo();
|
||||
const { setMode } = useColorScheme();
|
||||
const { i18n } = useTranslation();
|
||||
const userStore = useUserStore();
|
||||
@ -18,7 +20,7 @@ const Root: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser) {
|
||||
navigate("/auth", {
|
||||
navigateTo("/auth", {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
@ -33,7 +35,7 @@ const Root: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEqual(currentUserSetting.locale, "LOCALE_ZH")) {
|
||||
if (isEqual(currentUserSetting.locale, UserSetting_Locale.LOCALE_ZH)) {
|
||||
i18n.changeLanguage("zh");
|
||||
} else {
|
||||
i18n.changeLanguage("en");
|
||||
@ -53,6 +55,7 @@ const Root: React.FC = () => {
|
||||
{isInitialized && (
|
||||
<div className="w-full h-auto flex flex-col justify-start items-start dark:bg-zinc-900">
|
||||
<Header />
|
||||
<Navigator />
|
||||
<Outlet />
|
||||
</div>
|
||||
)}
|
||||
|
92
frontend/web/src/pages/CollectionDashboard.tsx
Normal file
92
frontend/web/src/pages/CollectionDashboard.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { Button } from "@mui/joy";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CollectionView from "@/components/CollectionView";
|
||||
import CreateCollectionDialog from "@/components/CreateCollectionDialog";
|
||||
import { shortcutService } from "@/services";
|
||||
import useCollectionStore from "@/stores/v1/collection";
|
||||
import FilterView from "../components/FilterView";
|
||||
import Icon from "../components/Icon";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
|
||||
interface State {
|
||||
showCreateCollectionDialog: boolean;
|
||||
}
|
||||
|
||||
const CollectionDashboard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const loadingState = useLoading();
|
||||
const collectionStore = useCollectionStore();
|
||||
const collections = collectionStore.getCollectionList();
|
||||
const [state, setState] = useState<State>({
|
||||
showCreateCollectionDialog: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([shortcutService.getMyAllShortcuts(), collectionStore.fetchCollectionList()]).finally(() => {
|
||||
loadingState.setFinish();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setShowCreateCollectionDialog = (show: boolean) => {
|
||||
setState({
|
||||
...state,
|
||||
showCreateCollectionDialog: show,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto max-w-8xl w-full px-3 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start">
|
||||
<div className="w-full flex flex-row justify-start items-start mb-4">
|
||||
<div className="bg-yellow-100 dark:bg-yellow-500 dark:opacity-70 py-2 px-3 rounded-full border dark:border-yellow-600 flex flex-row justify-start items-center cursor-pointer shadow">
|
||||
<Icon.LibrarySquare className="w-5 h-auto opacity-60" />
|
||||
<a
|
||||
className="hover:underline hover:text-blue-600"
|
||||
href="https://github.com/boojack/slash/blob/main/docs/getting-started/collections.md"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="mx-1 text-sm">Collection is in Beta. Learn more in docs</span>
|
||||
<Icon.ExternalLink className="w-4 h-auto inline-block" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-between items-center mb-4">
|
||||
<div className="flex flex-row justify-start items-center">
|
||||
<Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateCollectionDialog(true)}>
|
||||
<Icon.Plus className="w-5 h-auto" />
|
||||
<span className="ml-0.5">{t("common.create")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<FilterView />
|
||||
{loadingState.isLoading ? (
|
||||
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80 dark:text-gray-500">
|
||||
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
) : collections.length === 0 ? (
|
||||
<div className="py-16 w-full flex flex-col justify-center items-center text-gray-400">
|
||||
<Icon.PackageOpen className="w-16 h-auto" strokeWidth="1" />
|
||||
<p className="mt-4">No collections found.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex flex-col justify-start items-start gap-3">
|
||||
{collections.map((collection) => {
|
||||
return <CollectionView key={collection.id} collection={collection} />;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{state.showCreateCollectionDialog && (
|
||||
<CreateCollectionDialog
|
||||
onClose={() => setShowCreateCollectionDialog(false)}
|
||||
onConfirm={() => setShowCreateCollectionDialog(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionDashboard;
|
138
frontend/web/src/pages/CollectionSpace.tsx
Normal file
138
frontend/web/src/pages/CollectionSpace.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { Divider } from "@mui/joy";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import Icon from "@/components/Icon";
|
||||
import ShortcutView from "@/components/ShortcutView";
|
||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||
import useCollectionStore from "@/stores/v1/collection";
|
||||
import useShortcutStore from "@/stores/v1/shortcut";
|
||||
import useUserStore from "@/stores/v1/user";
|
||||
import { Collection } from "@/types/proto/api/v2/collection_service";
|
||||
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
import { convertShortcutFromPb } from "@/utils/shortcut";
|
||||
|
||||
const CollectionSpace = () => {
|
||||
const { collectionName } = useParams();
|
||||
const { sm } = useResponsiveWidth();
|
||||
const userStore = useUserStore();
|
||||
const collectionStore = useCollectionStore();
|
||||
const shortcutStore = useShortcutStore();
|
||||
const [collection, setCollection] = useState<Collection>();
|
||||
const [shortcuts, setShortcuts] = useState<Shortcut[]>([]);
|
||||
const [selectedShortcut, setSelectedShortcut] = useState<Shortcut>();
|
||||
|
||||
if (!collectionName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const collection = await collectionStore.fetchCollectionByName(collectionName);
|
||||
await userStore.getOrFetchUserById(collection.creatorId);
|
||||
setCollection(collection);
|
||||
setShortcuts([]);
|
||||
for (const shortcutId of collection.shortcutIds) {
|
||||
try {
|
||||
const shortcut = await shortcutStore.getOrFetchShortcutById(shortcutId);
|
||||
setShortcuts((shortcuts) => {
|
||||
return [...shortcuts, shortcut];
|
||||
});
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
}
|
||||
document.title = `${collection.title} - Slash`;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.details);
|
||||
}
|
||||
})();
|
||||
}, [collectionName]);
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const creator = userStore.getUserById(collection.creatorId);
|
||||
|
||||
const handleShortcutClick = (shortcut: Shortcut) => {
|
||||
if (sm) {
|
||||
setSelectedShortcut(shortcut);
|
||||
} else {
|
||||
window.open(`/s/${shortcut.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full sm:px-12 sm:py-10 sm:h-screen sm:bg-gray-100 dark:sm:bg-zinc-800">
|
||||
<div className="w-full h-full flex flex-row sm:border dark:sm:border-zinc-800 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900">
|
||||
<div className="w-full sm:w-56 sm:pr-4 flex flex-col justify-start items-start overflow-auto shrink-0">
|
||||
<div className="w-full sticky top-0 bg-gray-50 dark:bg-zinc-900">
|
||||
<div className="w-full flex flex-row justify-start items-center text-gray-800 dark:text-gray-300">
|
||||
<Icon.LibrarySquare className="w-5 h-auto mr-2 opacity-70" />
|
||||
<span className="text-lg">{collection.title}</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm">{collection.description}</p>
|
||||
<Divider className="!my-2" />
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-start items-start gap-2 sm:gap-1 px-px">
|
||||
{shortcuts.map((shortcut) => {
|
||||
return (
|
||||
<ShortcutView
|
||||
className={classNames(
|
||||
"w-full py-2 cursor-pointer",
|
||||
selectedShortcut?.id === shortcut.id
|
||||
? "bg-gray-100 dark:bg-zinc-800"
|
||||
: "sm:border-transparent dark:sm:border-transparent"
|
||||
)}
|
||||
key={shortcut.name}
|
||||
shortcut={convertShortcutFromPb(shortcut)}
|
||||
alwaysShowLink={!sm}
|
||||
onClick={() => handleShortcutClick(shortcut)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{sm && (
|
||||
<div className="w-full h-full overflow-clip rounded-lg border dark:border-zinc-800 bg-white dark:bg-zinc-800">
|
||||
{selectedShortcut ? (
|
||||
<div className="w-full h-full flex flex-col justify-center items-center p-8">
|
||||
<Link
|
||||
className="w-72 max-w-full border dark:border-zinc-900 dark:bg-zinc-900 p-6 rounded-2xl shadow-xl dark:text-gray-400 hover:opacity-80"
|
||||
to={`/s/${selectedShortcut.name}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon.Globe2Icon className="w-12 h-auto mb-1" strokeWidth={1} />
|
||||
<p className="text-lg font-medium leading-8">{selectedShortcut.title || selectedShortcut.name}</p>
|
||||
<p className="text-gray-500">{selectedShortcut.description}</p>
|
||||
<Divider className="!my-2" />
|
||||
<p className="text-gray-400 dark:text-gray-600 text-sm mt-2">
|
||||
<span className="leading-4">Open this site in a new tab</span>
|
||||
<Icon.ArrowUpRight className="inline-block ml-1 -mt-0.5 w-4 h-auto" />
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col justify-center items-center p-8">
|
||||
<div className="w-72 max-w-full border dark:border-zinc-900 dark:bg-zinc-900 dark:text-gray-400 p-6 rounded-2xl shadow-xl">
|
||||
<Icon.AppWindow className="w-12 h-auto mb-2" strokeWidth={1} />
|
||||
<p className="text-lg font-medium">Click on a tab in the Sidebar to get started.</p>
|
||||
<Divider className="!my-2" />
|
||||
<p className="text-gray-400 dark:text-gray-600 text-sm mt-2">
|
||||
Shared by <span className="italic font-medium">{creator.nickname}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionSpace;
|
@ -4,8 +4,8 @@ import { useTranslation } from "react-i18next";
|
||||
import CreateShortcutDialog from "../components/CreateShortcutDialog";
|
||||
import FilterView from "../components/FilterView";
|
||||
import Icon from "../components/Icon";
|
||||
import Navigator from "../components/Navigator";
|
||||
import ShortcutsContainer from "../components/ShortcutsContainer";
|
||||
import ShortcutsNavigator from "../components/ShortcutsNavigator";
|
||||
import ViewSetting from "../components/ViewSetting";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { shortcutService } from "../services";
|
||||
@ -45,8 +45,8 @@ const Home: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto max-w-6xl w-full px-3 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start">
|
||||
<Navigator />
|
||||
<div className="mx-auto max-w-8xl w-full px-3 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start">
|
||||
<ShortcutsNavigator />
|
||||
<div className="w-full flex flex-row justify-between items-center mb-4">
|
||||
<div className="flex flex-row justify-start items-center">
|
||||
<Button className="hover:shadow" variant="soft" size="sm" onClick={() => setShowCreateShortcutDialog(true)}>
|
||||
@ -69,9 +69,9 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
<FilterView />
|
||||
{loadingState.isLoading ? (
|
||||
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80">
|
||||
<div className="py-12 w-full flex flex-row justify-center items-center opacity-80 dark:text-gray-500">
|
||||
<Icon.Loader className="mr-2 w-5 h-auto animate-spin" />
|
||||
loading
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
) : orderedShortcutList.length === 0 ? (
|
||||
<div className="py-16 w-full flex flex-col justify-center items-center text-gray-400">
|
||||
|
62
frontend/web/src/pages/NotFound.tsx
Normal file
62
frontend/web/src/pages/NotFound.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { Button } from "@mui/joy";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import CreateShortcutDialog from "@/components/CreateShortcutDialog";
|
||||
import Icon from "@/components/Icon";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import useUserStore from "@/stores/v1/user";
|
||||
|
||||
interface State {
|
||||
showCreateShortcutButton: boolean;
|
||||
}
|
||||
|
||||
const NotFound = () => {
|
||||
const location = useLocation();
|
||||
const navigateTo = useNavigateTo();
|
||||
const currentUser = useUserStore().getCurrentUser();
|
||||
const [state, setState] = useState<State>({
|
||||
showCreateShortcutButton: false,
|
||||
});
|
||||
const [showCreateShortcutDialog, setShowCreateShortcutDialog] = useState(false);
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
useEffect(() => {
|
||||
const shortcut = params.get("shortcut");
|
||||
if (currentUser && shortcut) {
|
||||
setState({
|
||||
...state,
|
||||
showCreateShortcutButton: true,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
|
||||
<div className="w-full h-full flex flex-col justify-center items-center">
|
||||
<Icon.Meh strokeWidth={1} className="w-20 h-auto opacity-80 dark:text-gray-300" />
|
||||
<p className="mt-4 mb-8 text-4xl font-mono dark:text-gray-300">404</p>
|
||||
{state.showCreateShortcutButton && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startDecorator={<Icon.Plus className="w-5 h-auto" />}
|
||||
onClick={() => setShowCreateShortcutDialog(true)}
|
||||
>
|
||||
Create shortcut
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCreateShortcutDialog && (
|
||||
<CreateShortcutDialog
|
||||
initialShortcut={{ name: params.get("shortcut") || "" }}
|
||||
onClose={() => setShowCreateShortcutDialog(false)}
|
||||
onConfirm={() => navigateTo("/")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
@ -1,10 +1,11 @@
|
||||
import { Tooltip } from "@mui/joy";
|
||||
import classNames from "classnames";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLoaderData, useNavigate } from "react-router-dom";
|
||||
import { useLoaderData } from "react-router-dom";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { showCommonDialog } from "../components/Alert";
|
||||
import AnalyticsView from "../components/AnalyticsView";
|
||||
import CreateShortcutDialog from "../components/CreateShortcutDialog";
|
||||
@ -12,9 +13,8 @@ import GenerateQRCodeDialog from "../components/GenerateQRCodeDialog";
|
||||
import Icon from "../components/Icon";
|
||||
import VisibilityIcon from "../components/VisibilityIcon";
|
||||
import Dropdown from "../components/common/Dropdown";
|
||||
import { absolutifyLink } from "../helpers/utils";
|
||||
import { absolutifyLink, getFaviconWithGoogleS2 } from "../helpers/utils";
|
||||
import { shortcutService } from "../services";
|
||||
import useFaviconStore from "../stores/v1/favicon";
|
||||
import useUserStore from "../stores/v1/user";
|
||||
|
||||
interface State {
|
||||
@ -23,26 +23,17 @@ interface State {
|
||||
|
||||
const ShortcutDetail = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigateTo = useNavigateTo();
|
||||
const shortcutId = (useLoaderData() as Shortcut).id;
|
||||
const shortcut = shortcutService.getShortcutById(shortcutId) as Shortcut;
|
||||
const currentUser = useUserStore().getCurrentUser();
|
||||
const faviconStore = useFaviconStore();
|
||||
const [state, setState] = useState<State>({
|
||||
showEditModal: false,
|
||||
});
|
||||
const [favicon, setFavicon] = useState<string | undefined>(undefined);
|
||||
const [showQRCodeDialog, setShowQRCodeDialog] = useState<boolean>(false);
|
||||
const havePermission = currentUser.role === "ADMIN" || shortcut.creatorId === currentUser.id;
|
||||
const shortcutLink = absolutifyLink(`/s/${shortcut.name}`);
|
||||
|
||||
useEffect(() => {
|
||||
faviconStore.getOrFetchUrlFavicon(shortcut.link).then((url) => {
|
||||
if (url) {
|
||||
setFavicon(url);
|
||||
}
|
||||
});
|
||||
}, [shortcut.link]);
|
||||
const favicon = getFaviconWithGoogleS2(shortcut.link);
|
||||
|
||||
const handleCopyButtonClick = () => {
|
||||
copy(shortcutLink);
|
||||
@ -56,7 +47,7 @@ const ShortcutDetail = () => {
|
||||
style: "danger",
|
||||
onConfirm: async () => {
|
||||
await shortcutService.deleteShortcutById(shortcut.id);
|
||||
navigate("/", {
|
||||
navigateTo("/", {
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
@ -65,12 +56,12 @@ const ShortcutDetail = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto max-w-6xl w-full px-3 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start">
|
||||
<div className="mx-auto max-w-8xl w-full px-3 md:px-12 pt-4 pb-6 flex flex-col justify-start items-start">
|
||||
<div className="mt-8 w-12 h-12 flex justify-center items-center overflow-clip">
|
||||
{favicon ? (
|
||||
<img className="w-full h-auto rounded-full" src={favicon} decoding="async" loading="lazy" />
|
||||
<img className="w-full h-auto rounded-lg" src={favicon} decoding="async" loading="lazy" />
|
||||
) : (
|
||||
<Icon.CircleSlash className="w-full h-auto text-gray-400" />
|
||||
<Icon.CircleSlash className="w-full h-auto text-gray-400" strokeWidth={1} />
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
@ -81,9 +72,11 @@ const ShortcutDetail = () => {
|
||||
target="_blank"
|
||||
>
|
||||
<div className="truncate text-3xl">
|
||||
<span>{shortcut.title}</span>
|
||||
{shortcut.title ? (
|
||||
<>
|
||||
<span>{shortcut.title}</span>
|
||||
<span className="text-gray-400">(s/{shortcut.name})</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gray-400 dark:text-gray-500">s/</span>
|
||||
@ -146,14 +139,12 @@ const ShortcutDetail = () => {
|
||||
<div className="mt-4 ml-1 flex flex-row justify-start items-start flex-wrap gap-2">
|
||||
{shortcut.tags.map((tag) => {
|
||||
return (
|
||||
<span key={tag} className="max-w-[8rem] truncate text-gray-400 text font-mono leading-4 dark:text-gray-500">
|
||||
<span key={tag} className="max-w-[8rem] truncate text-gray-400 text leading-4 dark:text-gray-500">
|
||||
#{tag}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{shortcut.tags.length === 0 && (
|
||||
<span className="text-gray-400 text-sm font-mono leading-4 italic dark:text-gray-500">No tags</span>
|
||||
)}
|
||||
{shortcut.tags.length === 0 && <span className="text-gray-400 text-sm leading-4 italic dark:text-gray-500">No tags</span>}
|
||||
</div>
|
||||
<div className="w-full flex mt-4 gap-2">
|
||||
<Tooltip title="Creator" variant="solid" placement="top" arrow>
|
||||
|
@ -2,7 +2,8 @@ import { Button, Input } from "@mui/joy";
|
||||
import React, { FormEvent, useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import useWorkspaceStore from "@/stores/v1/workspace";
|
||||
import * as api from "../helpers/api";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
@ -10,7 +11,7 @@ import useUserStore from "../stores/v1/user";
|
||||
|
||||
const SignIn: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigateTo = useNavigateTo();
|
||||
const userStore = useUserStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const [email, setEmail] = useState("");
|
||||
@ -20,7 +21,7 @@ const SignIn: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (userStore.getCurrentUser()) {
|
||||
return navigate("/", {
|
||||
return navigateTo("/", {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
@ -52,7 +53,7 @@ const SignIn: React.FC = () => {
|
||||
await api.signin(email, password);
|
||||
const user = await userStore.fetchCurrentUser();
|
||||
if (user) {
|
||||
navigate("/", {
|
||||
navigateTo("/", {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
@ -70,7 +71,7 @@ const SignIn: React.FC = () => {
|
||||
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-start items-center">
|
||||
<div className="w-full py-4 grow flex flex-col justify-center items-center">
|
||||
<div className="flex flex-row justify-start items-center w-auto mx-auto gap-y-2 mb-4">
|
||||
<img id="logo-img" src="/logo.png" className="w-12 h-auto mr-2 -mt-1" alt="logo" />
|
||||
<img id="logo-img" src="/logo.png" className="w-12 h-auto mr-2 -mt-1 rounded-full shadow" alt="logo" />
|
||||
<span className="text-3xl opacity-80 dark:text-gray-500">Slash</span>
|
||||
</div>
|
||||
<form className="w-full mt-6" onSubmit={handleSigninBtnClick}>
|
||||
@ -99,15 +100,15 @@ const SignIn: React.FC = () => {
|
||||
disabled={actionBtnLoadingState.isLoading || !allowConfirm}
|
||||
onClick={handleSigninBtnClick}
|
||||
>
|
||||
Sign in
|
||||
{t("auth.sign-in")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{workspaceStore.setting.enableSignup && (
|
||||
{workspaceStore.profile.enableSignup && (
|
||||
<p className="w-full mt-4 text-sm">
|
||||
<span className="dark:text-gray-500">{"Don't have an account yet?"}</span>
|
||||
<Link to="/auth/signup" className="cursor-pointer ml-2 text-blue-600 hover:underline">
|
||||
Sign up
|
||||
{t("auth.sign-up")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
@ -2,7 +2,8 @@ import { Button, Input } from "@mui/joy";
|
||||
import React, { FormEvent, useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import useWorkspaceStore from "@/stores/v1/workspace";
|
||||
import * as api from "../helpers/api";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
@ -10,7 +11,7 @@ import useUserStore from "../stores/v1/user";
|
||||
|
||||
const SignUp: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const navigateTo = useNavigateTo();
|
||||
const userStore = useUserStore();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const [email, setEmail] = useState("");
|
||||
@ -21,13 +22,13 @@ const SignUp: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (userStore.getCurrentUser()) {
|
||||
return navigate("/", {
|
||||
return navigateTo("/", {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!workspaceStore.setting.enableSignup) {
|
||||
return navigate("/auth", {
|
||||
if (!workspaceStore.profile.enableSignup) {
|
||||
return navigateTo("/auth", {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
@ -59,7 +60,7 @@ const SignUp: React.FC = () => {
|
||||
await api.signup(email, nickname, password);
|
||||
const user = await userStore.fetchCurrentUser();
|
||||
if (user) {
|
||||
navigate("/", {
|
||||
navigateTo("/", {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
@ -77,10 +78,10 @@ const SignUp: React.FC = () => {
|
||||
<div className="w-80 max-w-full h-full py-4 flex flex-col justify-start items-center">
|
||||
<div className="w-full py-4 grow flex flex-col justify-center items-center">
|
||||
<div className="flex flex-row justify-start items-center w-auto mx-auto gap-y-2 mb-4">
|
||||
<img id="logo-img" src="/logo.png" className="w-12 h-auto mr-2 -mt-1" alt="logo" />
|
||||
<img id="logo-img" src="/logo.png" className="w-12 h-auto mr-2 -mt-1 rounded-full shadow" alt="logo" />
|
||||
<span className="text-3xl opacity-80 dark:text-gray-500">Slash</span>
|
||||
</div>
|
||||
<p className="w-full text-2xl mt-6 dark:text-gray-500">Create your account</p>
|
||||
<p className="w-full text-2xl mt-6 dark:text-gray-500">{t("auth.create-your-account")}</p>
|
||||
<form className="w-full mt-4" onSubmit={handleSignupBtnClick}>
|
||||
<div className={`flex flex-col justify-start items-start w-full ${actionBtnLoadingState.isLoading ? "opacity-80" : ""}`}>
|
||||
<div className="w-full flex flex-col mb-2">
|
||||
@ -111,14 +112,14 @@ const SignUp: React.FC = () => {
|
||||
disabled={actionBtnLoadingState.isLoading || !allowConfirm}
|
||||
onClick={handleSignupBtnClick}
|
||||
>
|
||||
Sign up
|
||||
{t("auth.sign-up")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="w-full mt-4 text-sm">
|
||||
<span className="dark:text-gray-500">{"Already has an account?"}</span>
|
||||
<Link to="/auth" className="cursor-pointer ml-2 text-blue-600 hover:underline">
|
||||
Sign in
|
||||
{t("auth.sign-in")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Button, Divider, Link, Textarea } from "@mui/joy";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, Button, Divider, Link, Textarea } from "@mui/joy";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Icon from "@/components/Icon";
|
||||
import SubscriptionFAQ from "@/components/SubscriptionFAQ";
|
||||
@ -16,17 +16,12 @@ const SubscriptionSetting: React.FC = () => {
|
||||
const isAdmin = currentUser.role === "ADMIN";
|
||||
const profile = workspaceStore.profile;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleUpdateLicenseKey = async () => {
|
||||
if (!isAdmin) {
|
||||
toast.error("Only admin can upload license key");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { subscription } = await subscriptionServiceClient.updateSubscription({
|
||||
licenseKey,
|
||||
@ -42,7 +37,7 @@ const SubscriptionSetting: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl w-full px-3 md:px-12 pt-8 pb-24 flex flex-col justify-start items-start gap-y-12">
|
||||
<div className="mx-auto max-w-8xl w-full px-3 md:px-12 pt-8 pb-24 flex flex-col justify-start items-start gap-y-12">
|
||||
<div className="w-full">
|
||||
<p className="text-base font-semibold leading-6 text-gray-900 dark:text-gray-500">Subscription</p>
|
||||
<div className="mt-2">
|
||||
@ -60,7 +55,7 @@ const SubscriptionSetting: React.FC = () => {
|
||||
<div className="w-full flex justify-between items-center mt-4">
|
||||
<div>
|
||||
{profile.plan === PlanType.FREE && (
|
||||
<Link href="https://yourselfhosted.lemonsqueezy.com/checkout?cart=df958121-81ad-4815-8b28-3f2d8e9c8e51" target="_blank">
|
||||
<Link href="https://yourselfhosted.lemonsqueezy.com/checkout/buy/d03a2696-8a8b-49c9-9e19-d425e3884fd7" target="_blank">
|
||||
Buy a license key
|
||||
<Icon.ExternalLink className="w-4 h-auto ml-1" />
|
||||
</Link>
|
||||
@ -74,6 +69,10 @@ const SubscriptionSetting: React.FC = () => {
|
||||
<Divider />
|
||||
<section className="w-full pb-8 dark:bg-zinc-900 flex items-center justify-center">
|
||||
<div className="w-full px-6">
|
||||
<Alert className="!inline-block mb-12">
|
||||
Slash is open source bookmarks and link sharing platform. Our source code is available and accessible on{" "}
|
||||
<Link href="https://github.com/boojack/slash">GitHub</Link> so anyone can get it, inspect it and review it.
|
||||
</Alert>
|
||||
<div className="w-full grid grid-cols-1 gap-12 mt-8 md:grid-cols-3">
|
||||
<div className="flex flex-col p-6 bg-white dark:bg-zinc-800 shadow-lg rounded-lg justify-between border border-gray-300 dark:border-zinc-700">
|
||||
<div>
|
||||
@ -138,7 +137,7 @@ const SubscriptionSetting: React.FC = () => {
|
||||
<Link
|
||||
className="w-full"
|
||||
underline="none"
|
||||
href="https://yourselfhosted.lemonsqueezy.com/checkout?cart=df958121-81ad-4815-8b28-3f2d8e9c8e51"
|
||||
href="https://yourselfhosted.lemonsqueezy.com/checkout/buy/d03a2696-8a8b-49c9-9e19-d425e3884fd7"
|
||||
target="_blank"
|
||||
>
|
||||
<Button className="w-full bg-gradient-to-r from-pink-500 to-purple-500 shadow hover:opacity-80">Get Started</Button>
|
||||
@ -172,7 +171,7 @@ const SubscriptionSetting: React.FC = () => {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Link className="w-full" underline="none" href="mailto:stevenlgtm@gmail.com" target="_blank">
|
||||
<Link className="w-full" underline="none" href="mailto:yourselfhosted@gmail.com" target="_blank">
|
||||
<Button className="w-full">Contact us</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import AccountSection from "../components/setting/AccountSection";
|
||||
|
||||
const Setting: React.FC = () => {
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl w-full px-3 md:px-12 py-6 flex flex-col justify-start items-start gap-y-12">
|
||||
<div className="mx-auto max-w-8xl w-full px-3 md:px-12 py-6 flex flex-col justify-start items-start gap-y-12">
|
||||
<AccountSection />
|
||||
<AccessTokenSection />
|
||||
<PreferenceSection />
|
||||
|
@ -25,7 +25,7 @@ const WorkspaceSetting: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl w-full px-3 md:px-12 py-6 flex flex-col justify-start items-start gap-y-12">
|
||||
<div className="mx-auto max-w-8xl w-full px-3 md:px-12 py-6 flex flex-col justify-start items-start gap-y-12">
|
||||
<Alert variant="soft" color="warning" startDecorator={<Icon.Info />}>
|
||||
You can see the settings items below because you are an Admin.
|
||||
</Alert>
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import CollectionDashboard from "@/pages/CollectionDashboard";
|
||||
import CollectionSpace from "@/pages/CollectionSpace";
|
||||
import NotFound from "@/pages/NotFound";
|
||||
import SignIn from "@/pages/SignIn";
|
||||
import SignUp from "@/pages/SignUp";
|
||||
import SubscriptionSetting from "@/pages/SubscriptionSetting";
|
||||
@ -16,11 +19,11 @@ const router = createBrowserRouter([
|
||||
element: <App />,
|
||||
children: [
|
||||
{
|
||||
path: "auth",
|
||||
path: "/auth",
|
||||
element: <SignIn />,
|
||||
},
|
||||
{
|
||||
path: "auth/signup",
|
||||
path: "/auth/signup",
|
||||
element: <SignUp />,
|
||||
},
|
||||
{
|
||||
@ -28,9 +31,13 @@ const router = createBrowserRouter([
|
||||
element: <Root />,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
path: "/",
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: "/collections",
|
||||
element: <CollectionDashboard />,
|
||||
},
|
||||
{
|
||||
path: "/shortcut/:shortcutId",
|
||||
element: <ShortcutDetail />,
|
||||
@ -53,6 +60,14 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "c/:collectionName",
|
||||
element: <CollectionSpace />,
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: <NotFound />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
@ -15,13 +15,6 @@ const shortcutService = {
|
||||
return store.getState().shortcut;
|
||||
},
|
||||
|
||||
fetchWorkspaceShortcuts: async () => {
|
||||
const data = (await api.getShortcutList({})).data;
|
||||
const shortcuts = data.map((s) => convertResponseModelShortcut(s));
|
||||
store.dispatch(setShortcuts(shortcuts));
|
||||
return shortcuts;
|
||||
},
|
||||
|
||||
getMyAllShortcuts: async () => {
|
||||
const data = (await api.getShortcutList()).data;
|
||||
const shortcuts = data.map((s) => convertResponseModelShortcut(s));
|
||||
|
104
frontend/web/src/stores/v1/collection.ts
Normal file
104
frontend/web/src/stores/v1/collection.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { create } from "zustand";
|
||||
import { collectionServiceClient } from "@/grpcweb";
|
||||
import { Collection } from "@/types/proto/api/v2/collection_service";
|
||||
|
||||
interface CollectionState {
|
||||
collectionMapById: Record<number, Collection>;
|
||||
fetchCollectionList: () => Promise<Collection[]>;
|
||||
getOrFetchCollectionById: (id: number) => Promise<Collection>;
|
||||
getCollectionById: (id: number) => Collection;
|
||||
getCollectionList: () => Collection[];
|
||||
fetchCollectionByName: (collectionName: string) => Promise<Collection>;
|
||||
createCollection: (collection: Collection) => Promise<Collection>;
|
||||
updateCollection: (collection: Partial<Collection>, updateMask: string[]) => Promise<Collection>;
|
||||
deleteCollection: (id: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const useCollectionStore = create<CollectionState>()((set, get) => ({
|
||||
collectionMapById: {},
|
||||
fetchCollectionList: async () => {
|
||||
const { collections } = await collectionServiceClient.listCollections({});
|
||||
const collectionMap = get().collectionMapById;
|
||||
collections.forEach((collection) => {
|
||||
collectionMap[collection.id] = collection;
|
||||
});
|
||||
set(collectionMap);
|
||||
return collections;
|
||||
},
|
||||
getOrFetchCollectionById: async (id: number) => {
|
||||
const collectionMap = get().collectionMapById;
|
||||
if (collectionMap[id]) {
|
||||
return collectionMap[id] as Collection;
|
||||
}
|
||||
|
||||
const { collection } = await collectionServiceClient.getCollection({
|
||||
id: id,
|
||||
});
|
||||
if (!collection) {
|
||||
throw new Error(`Collection with id ${id} not found`);
|
||||
}
|
||||
|
||||
collectionMap[id] = collection;
|
||||
set(collectionMap);
|
||||
return collection;
|
||||
},
|
||||
getCollectionById: (id: number) => {
|
||||
const collectionMap = get().collectionMapById;
|
||||
return collectionMap[id] as Collection;
|
||||
},
|
||||
getCollectionList: () => {
|
||||
return Object.values(get().collectionMapById);
|
||||
},
|
||||
fetchCollectionByName: async (collectionName: string) => {
|
||||
const { collection } = await collectionServiceClient.getCollectionByName({
|
||||
name: collectionName,
|
||||
});
|
||||
if (!collection) {
|
||||
throw new Error(`Collection with name ${collectionName} not found`);
|
||||
}
|
||||
|
||||
const collectionMap = get().collectionMapById;
|
||||
collectionMap[collection.id] = collection;
|
||||
set(collectionMap);
|
||||
return collection;
|
||||
},
|
||||
createCollection: async (collection: Collection) => {
|
||||
const { collection: createdCollection } = await collectionServiceClient.createCollection({
|
||||
collection: collection,
|
||||
});
|
||||
if (!createdCollection) {
|
||||
throw new Error(`Failed to create collection`);
|
||||
}
|
||||
|
||||
const collectionMap = get().collectionMapById;
|
||||
collectionMap[createdCollection.id] = createdCollection;
|
||||
set(collectionMap);
|
||||
return createdCollection;
|
||||
},
|
||||
updateCollection: async (collection: Partial<Collection>, updateMask: string[]) => {
|
||||
const { collection: updatedCollection } = await collectionServiceClient.updateCollection({
|
||||
collection: collection,
|
||||
updateMask: updateMask,
|
||||
});
|
||||
if (!updatedCollection) {
|
||||
throw new Error("Collection not found");
|
||||
}
|
||||
|
||||
console.log("updatedCollection", updatedCollection);
|
||||
|
||||
const collectionMap = get().collectionMapById;
|
||||
collectionMap[updatedCollection.id] = updatedCollection;
|
||||
set(collectionMap);
|
||||
return updatedCollection;
|
||||
},
|
||||
deleteCollection: async (id: number) => {
|
||||
await collectionServiceClient.deleteCollection({
|
||||
id,
|
||||
});
|
||||
const collectionMap = get().collectionMapById;
|
||||
delete collectionMap[id];
|
||||
set(collectionMap);
|
||||
},
|
||||
}));
|
||||
|
||||
export default useCollectionStore;
|
@ -1,41 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import * as api from "../../helpers/api";
|
||||
|
||||
interface FaviconState {
|
||||
cache: {
|
||||
[key: string]: string;
|
||||
};
|
||||
getOrFetchUrlFavicon: (url: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const useFaviconStore = create<FaviconState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
cache: {},
|
||||
getOrFetchUrlFavicon: async (url: string) => {
|
||||
const cache = get().cache;
|
||||
if (cache[url]) {
|
||||
return cache[url];
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: favicon } = await api.getUrlFavicon(url);
|
||||
if (favicon) {
|
||||
cache[url] = favicon;
|
||||
set(cache);
|
||||
return favicon;
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
return "";
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "favicon_cache",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useFaviconStore;
|
@ -1,30 +1,39 @@
|
||||
import { create } from "zustand";
|
||||
import * as api from "../../helpers/api";
|
||||
|
||||
const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
|
||||
return {
|
||||
...shortcut,
|
||||
createdTs: shortcut.createdTs * 1000,
|
||||
updatedTs: shortcut.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
import { shortcutServiceClient } from "@/grpcweb";
|
||||
import { Shortcut } from "@/types/proto/api/v2/shortcut_service";
|
||||
|
||||
interface ShortcutState {
|
||||
shortcutMapById: Record<ShortcutId, Shortcut>;
|
||||
fetchShortcutList: () => Promise<Shortcut[]>;
|
||||
getOrFetchShortcutById: (id: ShortcutId) => Promise<Shortcut>;
|
||||
getShortcutById: (id: ShortcutId) => Shortcut;
|
||||
getShortcutList: () => Shortcut[];
|
||||
}
|
||||
|
||||
const useShortcutStore = create<ShortcutState>()((set, get) => ({
|
||||
shortcutMapById: {},
|
||||
fetchShortcutList: async () => {
|
||||
const { shortcuts } = await shortcutServiceClient.listShortcuts({});
|
||||
const shortcutMap = get().shortcutMapById;
|
||||
shortcuts.forEach((shortcut) => {
|
||||
shortcutMap[shortcut.id] = shortcut;
|
||||
});
|
||||
set(shortcutMap);
|
||||
return shortcuts;
|
||||
},
|
||||
getOrFetchShortcutById: async (id: ShortcutId) => {
|
||||
const shortcutMap = get().shortcutMapById;
|
||||
if (shortcutMap[id]) {
|
||||
return shortcutMap[id] as Shortcut;
|
||||
}
|
||||
|
||||
const { data } = await api.getShortcutById(id);
|
||||
const shortcut = convertResponseModelShortcut(data);
|
||||
const { shortcut } = await shortcutServiceClient.getShortcut({
|
||||
id: id,
|
||||
});
|
||||
if (!shortcut) {
|
||||
throw new Error(`Shortcut with id ${id} not found`);
|
||||
}
|
||||
|
||||
shortcutMap[id] = shortcut;
|
||||
set(shortcutMap);
|
||||
return shortcut;
|
||||
@ -33,6 +42,9 @@ const useShortcutStore = create<ShortcutState>()((set, get) => ({
|
||||
const shortcutMap = get().shortcutMapById;
|
||||
return shortcutMap[id] as Shortcut;
|
||||
},
|
||||
getShortcutList: () => {
|
||||
return Object.values(get().shortcutMapById);
|
||||
},
|
||||
}));
|
||||
|
||||
export default useShortcutStore;
|
||||
|
15
frontend/web/src/utils/shortcut.ts
Normal file
15
frontend/web/src/utils/shortcut.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Shortcut as ShortcutPb } from "@/types/proto/api/v2/shortcut_service";
|
||||
|
||||
export const convertShortcutFromPb = (shortcutMessage: ShortcutPb): Shortcut => {
|
||||
return {
|
||||
id: shortcutMessage.id,
|
||||
creatorId: shortcutMessage.creatorId,
|
||||
createdTs: shortcutMessage.createdTime?.getTime() || 0,
|
||||
updatedTs: shortcutMessage.updatedTime?.getTime() || 0,
|
||||
name: shortcutMessage.name,
|
||||
link: shortcutMessage.link,
|
||||
title: shortcutMessage.title,
|
||||
description: shortcutMessage.description,
|
||||
tags: shortcutMessage.tags,
|
||||
} as Shortcut;
|
||||
};
|
13
frontend/web/src/utils/visibility.ts
Normal file
13
frontend/web/src/utils/visibility.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Visibility } from "@/types/proto/api/v2/common";
|
||||
|
||||
export const convertVisibilityFromPb = (visibility: Visibility): string => {
|
||||
if (visibility === Visibility.PRIVATE) {
|
||||
return "PRIVATE";
|
||||
} else if (visibility === Visibility.WORKSPACE) {
|
||||
return "WORKSPACE";
|
||||
} else if (visibility === Visibility.PUBLIC) {
|
||||
return "PUBLIC";
|
||||
} else {
|
||||
return "PRIVATE";
|
||||
}
|
||||
};
|
@ -14,6 +14,9 @@ module.exports = {
|
||||
"4xl": "2.25rem",
|
||||
},
|
||||
extend: {
|
||||
maxWidth: {
|
||||
"8xl": "88rem",
|
||||
},
|
||||
spacing: {
|
||||
112: "28rem",
|
||||
128: "32rem",
|
||||
|
@ -2,6 +2,12 @@ import react from "@vitejs/plugin-react-swc";
|
||||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
let devProxyServer = "http://localhost:8082/";
|
||||
if (process.env.DEV_PROXY_SERVER && process.env.DEV_PROXY_SERVER.length > 0) {
|
||||
console.log("Use devProxyServer from environment: ", process.env.DEV_PROXY_SERVER);
|
||||
devProxyServer = process.env.DEV_PROXY_SERVER;
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
@ -9,13 +15,17 @@ export default defineConfig({
|
||||
host: "0.0.0.0",
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/api/": {
|
||||
target: "http://localhost:8082/",
|
||||
changeOrigin: true,
|
||||
"^/api": {
|
||||
target: devProxyServer,
|
||||
xfwd: true,
|
||||
},
|
||||
"/s/": {
|
||||
target: "http://localhost:8082/",
|
||||
changeOrigin: true,
|
||||
"^/slash.api.v2": {
|
||||
target: devProxyServer,
|
||||
xfwd: true,
|
||||
},
|
||||
"^/s/": {
|
||||
target: devProxyServer,
|
||||
xfwd: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
93
go.mod
93
go.mod
@ -2,92 +2,87 @@ module github.com/boojack/slash
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/google/uuid v1.3.0
|
||||
require github.com/google/uuid v1.4.0
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.11.0
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/labstack/echo/v4 v4.10.1
|
||||
github.com/labstack/echo/v4 v4.11.2
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.6.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/friendsofgo/errors v0.9.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.11.7 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/rs/cors v1.7.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/rs/cors v1.10.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.10.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/tools v0.14.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.41.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.15 // indirect
|
||||
modernc.org/libc v1.30.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
nhooyr.io/websocket v1.8.6 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
nhooyr.io/websocket v1.8.10 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/improbable-eng/grpc-web v0.15.0
|
||||
github.com/mssola/useragent v1.0.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
go.deanishe.net/favicon v0.1.0
|
||||
go.uber.org/zap v1.21.0
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
|
||||
golang.org/x/mod v0.11.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e
|
||||
google.golang.org/grpc v1.57.0
|
||||
github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
golang.org/x/mod v0.14.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
modernc.org/sqlite v1.23.1
|
||||
modernc.org/sqlite v1.27.0
|
||||
)
|
||||
|
300
go.sum
300
go.sum
@ -39,8 +39,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArdnx94=
|
||||
github.com/PuerkitoBio/goquery v1.6.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
@ -50,29 +48,24 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@ -89,11 +82,12 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
|
||||
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
@ -114,17 +108,13 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
|
||||
github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@ -135,47 +125,14 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
|
||||
github.com/go-openapi/strfmt v0.19.8/go.mod h1:qBBipho+3EoIqn6YDI+4RnQEtj6jT/IdKm+PAlXxSUc=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
||||
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
||||
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
|
||||
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
|
||||
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
|
||||
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
|
||||
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
|
||||
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
|
||||
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
|
||||
github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
|
||||
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
|
||||
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
|
||||
github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
|
||||
github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
|
||||
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
|
||||
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
|
||||
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
|
||||
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
|
||||
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
|
||||
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
||||
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@ -186,8 +143,6 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
|
||||
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -217,7 +172,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@ -230,8 +184,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@ -250,10 +204,10 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
@ -262,14 +216,13 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 h1:dygLcbEBA+t/P7ck6a8AkXv6juQ4cK0RHBoh32jxhHM=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2/go.mod h1:Ap9RLCIJVtgQg1/BBgVEfypOAySvvlcpcVQkSzJCH4Y=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
@ -301,14 +254,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
|
||||
github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
|
||||
github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@ -316,25 +265,18 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
||||
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
|
||||
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@ -345,19 +287,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.10.1 h1:rB+D8In9PWjsp1OpHaqK+t04nQv/SBD1IoIcXCg0lpY=
|
||||
github.com/labstack/echo/v4 v4.10.1/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
|
||||
github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE=
|
||||
github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@ -367,10 +306,9 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@ -385,13 +323,9 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/mssola/useragent v1.0.0 h1:WRlDpXyxHDNfvZaPEut5Biveq86Ze4o4EMffyMxmH5o=
|
||||
github.com/mssola/useragent v1.0.0/go.mod h1:hz9Cqz4RXusgg1EdI4Al0INR62kP7aPSRNHnpU+b85Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@ -425,9 +359,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
@ -437,9 +370,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69 h1:01dHVodha5BzrMtVmcpPeA4VYbZEsTXQ6m4123zQXJk=
|
||||
github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69/go.mod h1:migYMxlAqcnQy+3eN8mcL0b2tpKy6R+8Zc0lxwk4dKM=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
@ -463,26 +399,26 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
|
||||
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
|
||||
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
@ -490,68 +426,56 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
|
||||
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
||||
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
|
||||
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.deanishe.net/favicon v0.1.0 h1:Afy941gjRik+DjUUcYHUxcztFEeFse2ITBkMMOlgefM=
|
||||
go.deanishe.net/favicon v0.1.0/go.mod h1:vIKVI+lUh8k3UAzaN4gjC+cpyatLQWmx0hVX4vLE8jU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.mongodb.org/mongo-driver v1.4.2/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@ -563,35 +487,29 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -603,8 +521,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -628,10 +546,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -672,10 +588,10 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -689,16 +605,14 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -709,13 +623,10 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -750,20 +661,17 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -772,14 +680,15 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
|
||||
golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -789,13 +698,9 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
@ -837,15 +742,12 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@ -913,12 +815,12 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8=
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e h1:S83+ibolgyZ0bqz7KEsUOPErxcv4VzlszxY+31OfB/E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
@ -941,8 +843,8 @@ google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -978,7 +880,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@ -992,36 +893,37 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
|
||||
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
|
||||
modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
|
||||
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/libc v1.30.0 h1:tw+o+UObwSE4Bfu3+Ztz9NW/Olqp7nTL/vcaEY/x4rc=
|
||||
modernc.org/libc v1.30.0/go.mod h1:SUKVISl2sU6aasM35Y0v4SsSBTt89uDKrvxgXkvsC/4=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8=
|
||||
modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
|
||||
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
111
proto/api/v2/collection_service.proto
Normal file
111
proto/api/v2/collection_service.proto
Normal file
@ -0,0 +1,111 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package slash.api.v2;
|
||||
|
||||
import "api/v2/common.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/client.proto";
|
||||
import "google/protobuf/field_mask.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "gen/api/v2";
|
||||
|
||||
service CollectionService {
|
||||
// ListCollections returns a list of collections.
|
||||
rpc ListCollections(ListCollectionsRequest) returns (ListCollectionsResponse) {
|
||||
option (google.api.http) = {get: "/api/v2/collections"};
|
||||
}
|
||||
// GetCollection returns a collection by id.
|
||||
rpc GetCollection(GetCollectionRequest) returns (GetCollectionResponse) {
|
||||
option (google.api.http) = {get: "/api/v2/collections/{id}"};
|
||||
option (google.api.method_signature) = "id";
|
||||
}
|
||||
// GetCollectionByName returns a collection by name.
|
||||
rpc GetCollectionByName(GetCollectionByNameRequest) returns (GetCollectionByNameResponse) {}
|
||||
// CreateCollection creates a collection.
|
||||
rpc CreateCollection(CreateCollectionRequest) returns (CreateCollectionResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v2/collections"
|
||||
body: "collection"
|
||||
};
|
||||
}
|
||||
// UpdateCollection updates a collection.
|
||||
rpc UpdateCollection(UpdateCollectionRequest) returns (UpdateCollectionResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/api/v2/collections/{collection.id}"
|
||||
body: "collection"
|
||||
};
|
||||
option (google.api.method_signature) = "collection,update_mask";
|
||||
}
|
||||
// DeleteCollection deletes a collection by id.
|
||||
rpc DeleteCollection(DeleteCollectionRequest) returns (DeleteCollectionResponse) {
|
||||
option (google.api.http) = {delete: "/api/v2/collections/{id}"};
|
||||
option (google.api.method_signature) = "id";
|
||||
}
|
||||
}
|
||||
|
||||
message Collection {
|
||||
int32 id = 1;
|
||||
|
||||
int32 creator_id = 2;
|
||||
|
||||
google.protobuf.Timestamp created_time = 3;
|
||||
|
||||
google.protobuf.Timestamp updated_time = 4;
|
||||
|
||||
string name = 6;
|
||||
|
||||
string title = 7;
|
||||
|
||||
string description = 8;
|
||||
|
||||
repeated int32 shortcut_ids = 9;
|
||||
|
||||
Visibility visibility = 10;
|
||||
}
|
||||
|
||||
message ListCollectionsRequest {}
|
||||
|
||||
message ListCollectionsResponse {
|
||||
repeated Collection collections = 1;
|
||||
}
|
||||
|
||||
message GetCollectionRequest {
|
||||
int32 id = 1;
|
||||
}
|
||||
|
||||
message GetCollectionResponse {
|
||||
Collection collection = 1;
|
||||
}
|
||||
|
||||
message GetCollectionByNameRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message GetCollectionByNameResponse {
|
||||
Collection collection = 1;
|
||||
}
|
||||
|
||||
message CreateCollectionRequest {
|
||||
Collection collection = 1;
|
||||
}
|
||||
|
||||
message CreateCollectionResponse {
|
||||
Collection collection = 1;
|
||||
}
|
||||
|
||||
message UpdateCollectionRequest {
|
||||
Collection collection = 1;
|
||||
|
||||
google.protobuf.FieldMask update_mask = 2;
|
||||
}
|
||||
|
||||
message UpdateCollectionResponse {
|
||||
Collection collection = 1;
|
||||
}
|
||||
|
||||
message DeleteCollectionRequest {
|
||||
int32 id = 1;
|
||||
}
|
||||
|
||||
message DeleteCollectionResponse {}
|
@ -11,3 +11,13 @@ enum RowStatus {
|
||||
|
||||
ARCHIVED = 2;
|
||||
}
|
||||
|
||||
enum Visibility {
|
||||
VISIBILITY_UNSPECIFIED = 0;
|
||||
|
||||
PRIVATE = 1;
|
||||
|
||||
WORKSPACE = 2;
|
||||
|
||||
PUBLIC = 3;
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ package slash.api.v2;
|
||||
import "api/v2/common.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/client.proto";
|
||||
import "google/protobuf/field_mask.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "gen/api/v2";
|
||||
|
||||
@ -13,10 +15,10 @@ service ShortcutService {
|
||||
rpc ListShortcuts(ListShortcutsRequest) returns (ListShortcutsResponse) {
|
||||
option (google.api.http) = {get: "/api/v2/shortcuts"};
|
||||
}
|
||||
// GetShortcut returns a shortcut by name.
|
||||
// GetShortcut returns a shortcut by id.
|
||||
rpc GetShortcut(GetShortcutRequest) returns (GetShortcutResponse) {
|
||||
option (google.api.http) = {get: "/api/v2/shortcuts/{name}"};
|
||||
option (google.api.method_signature) = "name";
|
||||
option (google.api.http) = {get: "/api/v2/shortcuts/{id}"};
|
||||
option (google.api.method_signature) = "id";
|
||||
}
|
||||
// CreateShortcut creates a shortcut.
|
||||
rpc CreateShortcut(CreateShortcutRequest) returns (CreateShortcutResponse) {
|
||||
@ -25,10 +27,18 @@ service ShortcutService {
|
||||
body: "shortcut"
|
||||
};
|
||||
}
|
||||
// DeleteShortcut deletes a shortcut by name.
|
||||
// UpdateShortcut updates a shortcut.
|
||||
rpc UpdateShortcut(UpdateShortcutRequest) returns (UpdateShortcutResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/api/v2/shortcuts/{shortcut.id}"
|
||||
body: "shortcut"
|
||||
};
|
||||
option (google.api.method_signature) = "shortcut,update_mask";
|
||||
}
|
||||
// DeleteShortcut deletes a shortcut by id.
|
||||
rpc DeleteShortcut(DeleteShortcutRequest) returns (DeleteShortcutResponse) {
|
||||
option (google.api.http) = {delete: "/api/v2/shortcuts/{name}"};
|
||||
option (google.api.method_signature) = "name";
|
||||
option (google.api.http) = {delete: "/api/v2/shortcuts/{id}"};
|
||||
option (google.api.method_signature) = "id";
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,9 +47,9 @@ message Shortcut {
|
||||
|
||||
int32 creator_id = 2;
|
||||
|
||||
int64 created_ts = 3;
|
||||
google.protobuf.Timestamp created_time = 3;
|
||||
|
||||
int64 updated_ts = 4;
|
||||
google.protobuf.Timestamp updated_time = 4;
|
||||
|
||||
RowStatus row_status = 5;
|
||||
|
||||
@ -55,7 +65,9 @@ message Shortcut {
|
||||
|
||||
Visibility visibility = 11;
|
||||
|
||||
OpenGraphMetadata og_metadata = 12;
|
||||
int32 view_count = 12;
|
||||
|
||||
OpenGraphMetadata og_metadata = 13;
|
||||
}
|
||||
|
||||
message OpenGraphMetadata {
|
||||
@ -66,16 +78,6 @@ message OpenGraphMetadata {
|
||||
string image = 3;
|
||||
}
|
||||
|
||||
enum Visibility {
|
||||
VISIBILITY_UNSPECIFIED = 0;
|
||||
|
||||
PRIVATE = 1;
|
||||
|
||||
WORKSPACE = 2;
|
||||
|
||||
PUBLIC = 3;
|
||||
}
|
||||
|
||||
message ListShortcutsRequest {}
|
||||
|
||||
message ListShortcutsResponse {
|
||||
@ -83,7 +85,7 @@ message ListShortcutsResponse {
|
||||
}
|
||||
|
||||
message GetShortcutRequest {
|
||||
string name = 1;
|
||||
int32 id = 1;
|
||||
}
|
||||
|
||||
message GetShortcutResponse {
|
||||
@ -98,8 +100,18 @@ message CreateShortcutResponse {
|
||||
Shortcut shortcut = 1;
|
||||
}
|
||||
|
||||
message UpdateShortcutRequest {
|
||||
Shortcut shortcut = 1;
|
||||
|
||||
google.protobuf.FieldMask update_mask = 2;
|
||||
}
|
||||
|
||||
message UpdateShortcutResponse {
|
||||
Shortcut shortcut = 1;
|
||||
}
|
||||
|
||||
message DeleteShortcutRequest {
|
||||
string name = 1;
|
||||
int32 id = 1;
|
||||
}
|
||||
|
||||
message DeleteShortcutResponse {}
|
||||
|
@ -5,6 +5,7 @@ package slash.api.v2;
|
||||
import "api/v2/common.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/client.proto";
|
||||
import "google/protobuf/field_mask.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "gen/api/v2";
|
||||
@ -28,10 +29,10 @@ service UserService {
|
||||
}
|
||||
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) {
|
||||
option (google.api.http) = {
|
||||
put: "/api/v2/users/{id}"
|
||||
patch: "/api/v2/users/{user.id}"
|
||||
body: "user"
|
||||
};
|
||||
option (google.api.method_signature) = "id";
|
||||
option (google.api.method_signature) = "user,update_mask";
|
||||
}
|
||||
// DeleteUser deletes a user by id.
|
||||
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {
|
||||
@ -63,9 +64,9 @@ message User {
|
||||
|
||||
RowStatus row_status = 2;
|
||||
|
||||
int64 created_ts = 3;
|
||||
google.protobuf.Timestamp created_time = 3;
|
||||
|
||||
int64 updated_ts = 4;
|
||||
google.protobuf.Timestamp updated_time = 4;
|
||||
|
||||
Role role = 6;
|
||||
|
||||
@ -107,11 +108,9 @@ message CreateUserResponse {
|
||||
}
|
||||
|
||||
message UpdateUserRequest {
|
||||
int32 id = 1;
|
||||
User user = 1;
|
||||
|
||||
User user = 2;
|
||||
|
||||
repeated string update_mask = 3;
|
||||
google.protobuf.FieldMask update_mask = 2;
|
||||
}
|
||||
|
||||
message UpdateUserResponse {
|
||||
|
@ -4,6 +4,7 @@ package slash.api.v2;
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/client.proto";
|
||||
import "google/protobuf/field_mask.proto";
|
||||
|
||||
option go_package = "gen/api/v2";
|
||||
|
||||
@ -16,10 +17,10 @@ service UserSettingService {
|
||||
// UpdateUserSetting updates the user setting.
|
||||
rpc UpdateUserSetting(UpdateUserSettingRequest) returns (UpdateUserSettingResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v2/users/{id}/settings"
|
||||
body: "*"
|
||||
patch: "/api/v2/users/{id}/settings"
|
||||
body: "user_setting"
|
||||
};
|
||||
option (google.api.method_signature) = "id";
|
||||
option (google.api.method_signature) = "user_setting,update_mask";
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +63,7 @@ message UpdateUserSettingRequest {
|
||||
UserSetting user_setting = 2;
|
||||
|
||||
// update_mask is the field mask to update.
|
||||
repeated string update_mask = 3;
|
||||
google.protobuf.FieldMask update_mask = 3;
|
||||
}
|
||||
|
||||
message UpdateUserSettingResponse {
|
||||
|
@ -4,6 +4,8 @@ package slash.api.v2;
|
||||
|
||||
import "api/v2/subscription_service.proto";
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/client.proto";
|
||||
import "google/protobuf/field_mask.proto";
|
||||
|
||||
option go_package = "gen/api/v2";
|
||||
|
||||
@ -16,9 +18,10 @@ service WorkspaceService {
|
||||
}
|
||||
rpc UpdateWorkspaceSetting(UpdateWorkspaceSettingRequest) returns (UpdateWorkspaceSettingResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/v2/workspace/setting"
|
||||
body: "*"
|
||||
patch: "/api/v2/workspace/setting"
|
||||
body: "setting"
|
||||
};
|
||||
option (google.api.method_signature) = "setting,update_mask";
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,14 +42,12 @@ message WorkspaceSetting {
|
||||
string license_key = 1;
|
||||
// Whether to enable other users to sign up.
|
||||
bool enable_signup = 2;
|
||||
// The relative path of the resource directory.
|
||||
string resource_relative_path = 3;
|
||||
// The custom style.
|
||||
string custom_style = 4;
|
||||
string custom_style = 3;
|
||||
// The custom script.
|
||||
string custom_script = 5;
|
||||
string custom_script = 4;
|
||||
// The auto backup setting.
|
||||
AutoBackupWorkspaceSetting auto_backup = 6;
|
||||
AutoBackupWorkspaceSetting auto_backup = 5;
|
||||
}
|
||||
|
||||
message AutoBackupWorkspaceSetting {
|
||||
@ -78,7 +79,7 @@ message UpdateWorkspaceSettingRequest {
|
||||
// The user setting.
|
||||
WorkspaceSetting setting = 1;
|
||||
// The update mask.
|
||||
repeated string update_mask = 2;
|
||||
google.protobuf.FieldMask update_mask = 2;
|
||||
}
|
||||
|
||||
message UpdateWorkspaceSettingResponse {
|
||||
|
@ -5,6 +5,24 @@
|
||||
|
||||
- [api/v2/common.proto](#api_v2_common-proto)
|
||||
- [RowStatus](#slash-api-v2-RowStatus)
|
||||
- [Visibility](#slash-api-v2-Visibility)
|
||||
|
||||
- [api/v2/collection_service.proto](#api_v2_collection_service-proto)
|
||||
- [Collection](#slash-api-v2-Collection)
|
||||
- [CreateCollectionRequest](#slash-api-v2-CreateCollectionRequest)
|
||||
- [CreateCollectionResponse](#slash-api-v2-CreateCollectionResponse)
|
||||
- [DeleteCollectionRequest](#slash-api-v2-DeleteCollectionRequest)
|
||||
- [DeleteCollectionResponse](#slash-api-v2-DeleteCollectionResponse)
|
||||
- [GetCollectionByNameRequest](#slash-api-v2-GetCollectionByNameRequest)
|
||||
- [GetCollectionByNameResponse](#slash-api-v2-GetCollectionByNameResponse)
|
||||
- [GetCollectionRequest](#slash-api-v2-GetCollectionRequest)
|
||||
- [GetCollectionResponse](#slash-api-v2-GetCollectionResponse)
|
||||
- [ListCollectionsRequest](#slash-api-v2-ListCollectionsRequest)
|
||||
- [ListCollectionsResponse](#slash-api-v2-ListCollectionsResponse)
|
||||
- [UpdateCollectionRequest](#slash-api-v2-UpdateCollectionRequest)
|
||||
- [UpdateCollectionResponse](#slash-api-v2-UpdateCollectionResponse)
|
||||
|
||||
- [CollectionService](#slash-api-v2-CollectionService)
|
||||
|
||||
- [api/v2/shortcut_service.proto](#api_v2_shortcut_service-proto)
|
||||
- [CreateShortcutRequest](#slash-api-v2-CreateShortcutRequest)
|
||||
@ -17,8 +35,8 @@
|
||||
- [ListShortcutsResponse](#slash-api-v2-ListShortcutsResponse)
|
||||
- [OpenGraphMetadata](#slash-api-v2-OpenGraphMetadata)
|
||||
- [Shortcut](#slash-api-v2-Shortcut)
|
||||
|
||||
- [Visibility](#slash-api-v2-Visibility)
|
||||
- [UpdateShortcutRequest](#slash-api-v2-UpdateShortcutRequest)
|
||||
- [UpdateShortcutResponse](#slash-api-v2-UpdateShortcutResponse)
|
||||
|
||||
- [ShortcutService](#slash-api-v2-ShortcutService)
|
||||
|
||||
@ -108,8 +126,247 @@
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-Visibility"></a>
|
||||
|
||||
### Visibility
|
||||
|
||||
|
||||
| Name | Number | Description |
|
||||
| ---- | ------ | ----------- |
|
||||
| VISIBILITY_UNSPECIFIED | 0 | |
|
||||
| PRIVATE | 1 | |
|
||||
| WORKSPACE | 2 | |
|
||||
| PUBLIC | 3 | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="api_v2_collection_service-proto"></a>
|
||||
<p align="right"><a href="#top">Top</a></p>
|
||||
|
||||
## api/v2/collection_service.proto
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-Collection"></a>
|
||||
|
||||
### Collection
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| id | [int32](#int32) | | |
|
||||
| creator_id | [int32](#int32) | | |
|
||||
| created_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
|
||||
| updated_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
|
||||
| name | [string](#string) | | |
|
||||
| title | [string](#string) | | |
|
||||
| description | [string](#string) | | |
|
||||
| shortcut_ids | [int32](#int32) | repeated | |
|
||||
| visibility | [Visibility](#slash-api-v2-Visibility) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-CreateCollectionRequest"></a>
|
||||
|
||||
### CreateCollectionRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| collection | [Collection](#slash-api-v2-Collection) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-CreateCollectionResponse"></a>
|
||||
|
||||
### CreateCollectionResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| collection | [Collection](#slash-api-v2-Collection) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-DeleteCollectionRequest"></a>
|
||||
|
||||
### DeleteCollectionRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| id | [int32](#int32) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-DeleteCollectionResponse"></a>
|
||||
|
||||
### DeleteCollectionResponse
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-GetCollectionByNameRequest"></a>
|
||||
|
||||
### GetCollectionByNameRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| name | [string](#string) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-GetCollectionByNameResponse"></a>
|
||||
|
||||
### GetCollectionByNameResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| collection | [Collection](#slash-api-v2-Collection) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-GetCollectionRequest"></a>
|
||||
|
||||
### GetCollectionRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| id | [int32](#int32) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-GetCollectionResponse"></a>
|
||||
|
||||
### GetCollectionResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| collection | [Collection](#slash-api-v2-Collection) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-ListCollectionsRequest"></a>
|
||||
|
||||
### ListCollectionsRequest
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-ListCollectionsResponse"></a>
|
||||
|
||||
### ListCollectionsResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| collections | [Collection](#slash-api-v2-Collection) | repeated | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-UpdateCollectionRequest"></a>
|
||||
|
||||
### UpdateCollectionRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| collection | [Collection](#slash-api-v2-Collection) | | |
|
||||
| update_mask | [google.protobuf.FieldMask](#google-protobuf-FieldMask) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-UpdateCollectionResponse"></a>
|
||||
|
||||
### UpdateCollectionResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| collection | [Collection](#slash-api-v2-Collection) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-CollectionService"></a>
|
||||
|
||||
### CollectionService
|
||||
|
||||
|
||||
| Method Name | Request Type | Response Type | Description |
|
||||
| ----------- | ------------ | ------------- | ------------|
|
||||
| ListCollections | [ListCollectionsRequest](#slash-api-v2-ListCollectionsRequest) | [ListCollectionsResponse](#slash-api-v2-ListCollectionsResponse) | ListCollections returns a list of collections. |
|
||||
| GetCollection | [GetCollectionRequest](#slash-api-v2-GetCollectionRequest) | [GetCollectionResponse](#slash-api-v2-GetCollectionResponse) | GetCollection returns a collection by id. |
|
||||
| GetCollectionByName | [GetCollectionByNameRequest](#slash-api-v2-GetCollectionByNameRequest) | [GetCollectionByNameResponse](#slash-api-v2-GetCollectionByNameResponse) | GetCollectionByName returns a collection by name. |
|
||||
| CreateCollection | [CreateCollectionRequest](#slash-api-v2-CreateCollectionRequest) | [CreateCollectionResponse](#slash-api-v2-CreateCollectionResponse) | CreateCollection creates a collection. |
|
||||
| UpdateCollection | [UpdateCollectionRequest](#slash-api-v2-UpdateCollectionRequest) | [UpdateCollectionResponse](#slash-api-v2-UpdateCollectionResponse) | UpdateCollection updates a collection. |
|
||||
| DeleteCollection | [DeleteCollectionRequest](#slash-api-v2-DeleteCollectionRequest) | [DeleteCollectionResponse](#slash-api-v2-DeleteCollectionResponse) | DeleteCollection deletes a collection by id. |
|
||||
|
||||
|
||||
|
||||
@ -160,7 +417,7 @@
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| name | [string](#string) | | |
|
||||
| id | [int32](#int32) | | |
|
||||
|
||||
|
||||
|
||||
@ -185,7 +442,7 @@
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| name | [string](#string) | | |
|
||||
| id | [int32](#int32) | | |
|
||||
|
||||
|
||||
|
||||
@ -259,8 +516,8 @@
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| id | [int32](#int32) | | |
|
||||
| creator_id | [int32](#int32) | | |
|
||||
| created_ts | [int64](#int64) | | |
|
||||
| updated_ts | [int64](#int64) | | |
|
||||
| created_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
|
||||
| updated_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
|
||||
| row_status | [RowStatus](#slash-api-v2-RowStatus) | | |
|
||||
| name | [string](#string) | | |
|
||||
| link | [string](#string) | | |
|
||||
@ -268,6 +525,7 @@
|
||||
| tags | [string](#string) | repeated | |
|
||||
| description | [string](#string) | | |
|
||||
| visibility | [Visibility](#slash-api-v2-Visibility) | | |
|
||||
| view_count | [int32](#int32) | | |
|
||||
| og_metadata | [OpenGraphMetadata](#slash-api-v2-OpenGraphMetadata) | | |
|
||||
|
||||
|
||||
@ -275,19 +533,36 @@
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-UpdateShortcutRequest"></a>
|
||||
|
||||
### UpdateShortcutRequest
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| shortcut | [Shortcut](#slash-api-v2-Shortcut) | | |
|
||||
| update_mask | [google.protobuf.FieldMask](#google-protobuf-FieldMask) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="slash-api-v2-UpdateShortcutResponse"></a>
|
||||
|
||||
### UpdateShortcutResponse
|
||||
|
||||
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| shortcut | [Shortcut](#slash-api-v2-Shortcut) | | |
|
||||
|
||||
|
||||
<a name="slash-api-v2-Visibility"></a>
|
||||
|
||||
### Visibility
|
||||
|
||||
|
||||
| Name | Number | Description |
|
||||
| ---- | ------ | ----------- |
|
||||
| VISIBILITY_UNSPECIFIED | 0 | |
|
||||
| PRIVATE | 1 | |
|
||||
| WORKSPACE | 2 | |
|
||||
| PUBLIC | 3 | |
|
||||
|
||||
|
||||
|
||||
@ -303,9 +578,10 @@
|
||||
| Method Name | Request Type | Response Type | Description |
|
||||
| ----------- | ------------ | ------------- | ------------|
|
||||
| ListShortcuts | [ListShortcutsRequest](#slash-api-v2-ListShortcutsRequest) | [ListShortcutsResponse](#slash-api-v2-ListShortcutsResponse) | ListShortcuts returns a list of shortcuts. |
|
||||
| GetShortcut | [GetShortcutRequest](#slash-api-v2-GetShortcutRequest) | [GetShortcutResponse](#slash-api-v2-GetShortcutResponse) | GetShortcut returns a shortcut by name. |
|
||||
| GetShortcut | [GetShortcutRequest](#slash-api-v2-GetShortcutRequest) | [GetShortcutResponse](#slash-api-v2-GetShortcutResponse) | GetShortcut returns a shortcut by id. |
|
||||
| CreateShortcut | [CreateShortcutRequest](#slash-api-v2-CreateShortcutRequest) | [CreateShortcutResponse](#slash-api-v2-CreateShortcutResponse) | CreateShortcut creates a shortcut. |
|
||||
| DeleteShortcut | [DeleteShortcutRequest](#slash-api-v2-DeleteShortcutRequest) | [DeleteShortcutResponse](#slash-api-v2-DeleteShortcutResponse) | DeleteShortcut deletes a shortcut by name. |
|
||||
| UpdateShortcut | [UpdateShortcutRequest](#slash-api-v2-UpdateShortcutRequest) | [UpdateShortcutResponse](#slash-api-v2-UpdateShortcutResponse) | UpdateShortcut updates a shortcut. |
|
||||
| DeleteShortcut | [DeleteShortcutRequest](#slash-api-v2-DeleteShortcutRequest) | [DeleteShortcutResponse](#slash-api-v2-DeleteShortcutResponse) | DeleteShortcut deletes a shortcut by id. |
|
||||
|
||||
|
||||
|
||||
@ -636,9 +912,8 @@
|
||||
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| id | [int32](#int32) | | |
|
||||
| user | [User](#slash-api-v2-User) | | |
|
||||
| update_mask | [string](#string) | repeated | |
|
||||
| update_mask | [google.protobuf.FieldMask](#google-protobuf-FieldMask) | | |
|
||||
|
||||
|
||||
|
||||
@ -670,8 +945,8 @@
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| id | [int32](#int32) | | |
|
||||
| row_status | [RowStatus](#slash-api-v2-RowStatus) | | |
|
||||
| created_ts | [int64](#int64) | | |
|
||||
| updated_ts | [int64](#int64) | | |
|
||||
| created_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
|
||||
| updated_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
|
||||
| role | [Role](#slash-api-v2-Role) | | |
|
||||
| email | [string](#string) | | |
|
||||
| nickname | [string](#string) | | |
|
||||
@ -786,7 +1061,7 @@
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| id | [int32](#int32) | | id is the user id. |
|
||||
| user_setting | [UserSetting](#slash-api-v2-UserSetting) | | user_setting is the user setting to update. |
|
||||
| update_mask | [string](#string) | repeated | update_mask is the field mask to update. |
|
||||
| update_mask | [google.protobuf.FieldMask](#google-protobuf-FieldMask) | | update_mask is the field mask to update. |
|
||||
|
||||
|
||||
|
||||
@ -955,7 +1230,7 @@
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| setting | [WorkspaceSetting](#slash-api-v2-WorkspaceSetting) | | The user setting. |
|
||||
| update_mask | [string](#string) | repeated | The update mask. |
|
||||
| update_mask | [google.protobuf.FieldMask](#google-protobuf-FieldMask) | | The update mask. |
|
||||
|
||||
|
||||
|
||||
@ -1006,7 +1281,6 @@
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| license_key | [string](#string) | | |
|
||||
| enable_signup | [bool](#bool) | | Whether to enable other users to sign up. |
|
||||
| resource_relative_path | [string](#string) | | The relative path of the resource directory. |
|
||||
| custom_style | [string](#string) | | The custom style. |
|
||||
| custom_script | [string](#string) | | The custom script. |
|
||||
| auto_backup | [AutoBackupWorkspaceSetting](#slash-api-v2-AutoBackupWorkspaceSetting) | | The auto backup setting. |
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user